/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * DiskImg nibblized read/write functions.
 */
#include "StdAfx.h"
#include "DiskImgPriv.h"

/* define this for verbose output */
//#define NIB_VERBOSE_DEBUG


/*
 * ===========================================================================
 *      Nibble encoding and decoding
 * ===========================================================================
 */

/*static*/ unsigned char DiskImg::kDiskBytes53[32] = {
    0xab, 0xad, 0xae, 0xaf, 0xb5, 0xb6, 0xb7, 0xba,
    0xbb, 0xbd, 0xbe, 0xbf, 0xd6, 0xd7, 0xda, 0xdb,
    0xdd, 0xde, 0xdf, 0xea, 0xeb, 0xed, 0xee, 0xef,
    0xf5, 0xf6, 0xf7, 0xfa, 0xfb, 0xfd, 0xfe, 0xff
};
/*static*/ unsigned char DiskImg::kDiskBytes62[64] = {
    0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
    0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3,
    0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc,
    0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, 0xd3,
    0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde,
    0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec,
    0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
    0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff
};
/*static*/ unsigned char DiskImg::kInvDiskBytes53[256]; // all values are 0-31
/*static*/ unsigned char DiskImg::kInvDiskBytes62[256];	// all values are 0-63

/*
 * Compute tables to convert disk bytes back to values.
 *
 * Should be called once, at DLL initialization time.
 */
/*static*/ void
DiskImg::CalcNibbleInvTables(void)
{
    unsigned int i;

    memset(kInvDiskBytes53, kInvInvalidValue, sizeof(kInvDiskBytes53));
    for (i = 0; i < sizeof(kDiskBytes53); i++) {
        assert(kDiskBytes53[i] >= 0x96);
        kInvDiskBytes53[kDiskBytes53[i]] = i;
    }

    memset(kInvDiskBytes62, kInvInvalidValue, sizeof(kInvDiskBytes62));
    for (i = 0; i < sizeof(kDiskBytes62); i++) {
        assert(kDiskBytes62[i] >= 0x96);
        kInvDiskBytes62[kDiskBytes62[i]] = i;
    }
}

/*
 * Find the start of the data field of a sector in nibblized data.
 *
 * Returns the index start on success or -1 on failure.
 */
int
DiskImg::FindNibbleSectorStart(const CircularBufferAccess& buffer, int track,
    int sector, const NibbleDescr* pNibbleDescr, int* pVol)
{
    const int kMaxDataReach = 48;       // fairly arbitrary
    //DIError dierr;
    long trackLen = buffer.GetSize();

    assert(sector >= 0 && sector < 16);

    int i;

    for (i = 0; i < trackLen; i++) {
		bool foundAddr = false;

		if (pNibbleDescr->special == kNibbleSpecialSkipFirstAddrByte) {
			if (/*buffer[i] == pNibbleDescr->addrProlog[0] &&*/
				buffer[i+1] == pNibbleDescr->addrProlog[1] &&
				buffer[i+2] == pNibbleDescr->addrProlog[2])
			{
				foundAddr = true;
			}
		} else {
			if (buffer[i] == pNibbleDescr->addrProlog[0] &&
				buffer[i+1] == pNibbleDescr->addrProlog[1] &&
				buffer[i+2] == pNibbleDescr->addrProlog[2])
			{
				foundAddr = true;
			}
		}

		if (foundAddr) {
            //i += 3;

            /* found the address header, decode the address */
            short hdrVol, hdrTrack, hdrSector, hdrChksum;
            DecodeAddr(buffer, i+3, &hdrVol, &hdrTrack, &hdrSector,
                &hdrChksum);

            if (pNibbleDescr->addrVerifyTrack && track != hdrTrack) {
                WMSG3("  Track mismatch (T=%d) got T=%d,S=%d\n",
                    track, hdrTrack, hdrSector);
                continue;
            }

            if (pNibbleDescr->addrVerifyChecksum) {
                if ((pNibbleDescr->addrChecksumSeed ^
                    hdrVol ^ hdrTrack ^ hdrSector ^ hdrChksum) != 0)
                {
                    WMSG4("   Addr checksum mismatch (want T=%d,S=%d, got "
                          "T=%d,S=%d)\n",
                        track, sector, hdrTrack, hdrSector);
                    continue;
                }
            }

			i += 3;

            int j;
            for (j = 0; j < pNibbleDescr->addrEpilogVerifyCount; j++) {
                if (buffer[i+8+j] != pNibbleDescr->addrEpilog[j]) {
                    //WMSG3("   Bad epilog byte %d (%02x vs %02x)\n",
                    //    j, buffer[i+8+j], pNibbleDescr->addrEpilog[j]);
                    break;
                }
            }
            if (j != pNibbleDescr->addrEpilogVerifyCount)
                continue;

#ifdef NIB_VERBOSE_DEBUG
            WMSG4("    Good header, T=%d,S=%d (looking for T=%d,S=%d)\n",
                hdrTrack, hdrSector, track, sector);
#endif

            if (pNibbleDescr->special == kNibbleSpecialMuse) {
                /* e.g. original Castle Wolfenstein */
                if (track > 2) {
                    if ((hdrSector & 0x01) != 0)
                        continue;
                    hdrSector /= 2;
                }
            }

            if (sector != hdrSector)
                continue;

            /*
             * Scan forward and look for data prolog.  We want to limit
             * the reach of our search so we don't blunder into the data
             * field of the next sector.
             */
            for (j = 0; j < kMaxDataReach; j++) {
                if (buffer[i + j] == pNibbleDescr->dataProlog[0] &&
                    buffer[i + j +1] == pNibbleDescr->dataProlog[1] &&
                    buffer[i + j +2] == pNibbleDescr->dataProlog[2])
                {
                    *pVol = hdrVol;
                    return buffer.Normalize(i + j + 3);
                }
            }
        }
    }

#ifdef NIB_VERBOSE_DEBUG
    WMSG2("   Couldn't find T=%d,S=%d\n", track, sector);
#endif
    return -1;
}

/*
 * Decode the values in the address field.
 */
void
DiskImg::DecodeAddr(const CircularBufferAccess& buffer, int offset,
    short* pVol, short* pTrack, short* pSector, short* pChksum)
{
    //unsigned int vol, track, sector, chksum;

    *pVol = ConvFrom44(buffer[offset], buffer[offset+1]);
    *pTrack = ConvFrom44(buffer[offset+2], buffer[offset+3]);
    *pSector = ConvFrom44(buffer[offset+4], buffer[offset+5]);
    *pChksum = ConvFrom44(buffer[offset+6], buffer[offset+7]);
}

/*
 * Decode the sector pointed to by "pData" and described by "pNibbleDescr".
 * This invokes the appropriate function (e.g. 5&3 or 6&2) to decode the
 * data into a 256-byte sector.
 */
DIError
DiskImg::DecodeNibbleData(const CircularBufferAccess& buffer, int idx,
    unsigned char* sctBuf, const NibbleDescr* pNibbleDescr)
{
    switch (pNibbleDescr->encoding) {
    case kNibbleEnc62:
        return DecodeNibble62(buffer, idx, sctBuf, pNibbleDescr);
    case kNibbleEnc53:
        return DecodeNibble53(buffer, idx, sctBuf, pNibbleDescr);
    default:
        assert(false);
        return kDIErrInternal;
    }
}

/*
 * Encode the sector pointed to by "pData" and described by "pNibbleDescr".
 * This invokes the appropriate function (e.g. 5&3 or 6&2) to encode the
 * data from a 256-byte sector.
 */
void
DiskImg::EncodeNibbleData(const CircularBufferAccess& buffer, int idx,
    const unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) const
{
    switch (pNibbleDescr->encoding) {
    case kNibbleEnc62:
        EncodeNibble62(buffer, idx, sctBuf, pNibbleDescr);
        break;
    case kNibbleEnc53:
        EncodeNibble53(buffer, idx, sctBuf, pNibbleDescr);
        break;
    default:
        assert(false);
        break;
    }
}

/*
 * Decode 6&2 encoding.
 */
DIError
DiskImg::DecodeNibble62(const CircularBufferAccess& buffer, int idx,
    unsigned char* sctBuf, const NibbleDescr* pNibbleDescr)
{
    unsigned char twos[kChunkSize62 * 3];   // 258
    int chksum = pNibbleDescr->dataChecksumSeed;
    unsigned char decodedVal;
    int i;

    /*
     * Pull the 342 bytes out, convert them from disk bytes to 6-bit
     * values, and arrange them into a DOS-like pair of buffers.
     */
    for (i = 0; i < kChunkSize62; i++) {
        decodedVal = kInvDiskBytes62[buffer[idx++]];
        if (decodedVal == kInvInvalidValue)
            return kDIErrInvalidDiskByte;
        assert(decodedVal < sizeof(kDiskBytes62));

        chksum ^= decodedVal;
        twos[i] =
            ((chksum & 0x01) << 1) | ((chksum & 0x02) >> 1);
        twos[i + kChunkSize62] =
            ((chksum & 0x04) >> 1) | ((chksum & 0x08) >> 3);
        twos[i + kChunkSize62*2] =
            ((chksum & 0x10) >> 3) | ((chksum & 0x20) >> 5);
    }

    for (i = 0; i < 256; i++) {
        decodedVal = kInvDiskBytes62[buffer[idx++]];
        if (decodedVal == kInvInvalidValue)
            return kDIErrInvalidDiskByte;
        assert(decodedVal < sizeof(kDiskBytes62));

        chksum ^= decodedVal;
        sctBuf[i] = (chksum << 2) | twos[i];
    }

    /*
     * Grab the 343rd byte (the checksum byte) and see if we did this
     * right.
     */
    //printf("Dec checksum value is 0x%02x\n", chksum);
    decodedVal = kInvDiskBytes62[buffer[idx++]];
    if (decodedVal == kInvInvalidValue)
        return kDIErrInvalidDiskByte;
    assert(decodedVal < sizeof(kDiskBytes62));
    chksum ^= decodedVal;

    if (pNibbleDescr->dataVerifyChecksum && chksum != 0) {
        WMSG0("    NIB bad data checksum\n");
        return kDIErrBadChecksum;
    }
    return kDIErrNone;
}

/*
 * Encode 6&2 encoding.
 */
void
DiskImg::EncodeNibble62(const CircularBufferAccess& buffer, int idx,
    const unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) const
{
    unsigned char top[256];
    unsigned char twos[kChunkSize62];
    int twoPosn, twoShift;
    int i;

    memset(twos, 0, sizeof(twos));

    twoShift = 0;
    for (i = 0, twoPosn = kChunkSize62-1; i < 256; i++) {
        unsigned int val = sctBuf[i];
        top[i] = val >> 2;
        twos[twoPosn] |= ((val & 0x01) << 1 | (val & 0x02) >> 1) << twoShift;

        if (twoPosn == 0) {
            twoPosn = kChunkSize62;
            twoShift += 2;
        }
        twoPosn--;
    }

    int chksum = pNibbleDescr->dataChecksumSeed;
    for (i = kChunkSize62-1; i >= 0; i--) {
        assert(twos[i] < sizeof(kDiskBytes62));
        buffer[idx++] = kDiskBytes62[twos[i] ^ chksum];
        chksum = twos[i];
    }

    for (i = 0; i < 256; i++) {
        assert(top[i] < sizeof(kDiskBytes62));
        buffer[idx++] = kDiskBytes62[top[i] ^ chksum];
        chksum = top[i];
    }

    //printf("Enc checksum value is 0x%02x\n", chksum);
    buffer[idx++] = kDiskBytes62[chksum];
}

/*
 * Decode 5&3 encoding.
 */
DIError
DiskImg::DecodeNibble53(const CircularBufferAccess& buffer, int idx,
    unsigned char* sctBuf, const NibbleDescr* pNibbleDescr)
{
    unsigned char base[256];
    unsigned char threes[kThreeSize];
    int chksum = pNibbleDescr->dataChecksumSeed;
    unsigned char decodedVal;
    int i;

    /*
     * Pull the 410 bytes out, convert them from disk bytes to 5-bit
     * values, and arrange them into a DOS-like pair of buffers.
     */
    for (i = kThreeSize-1; i >= 0; i--) {
        decodedVal = kInvDiskBytes53[buffer[idx++]];
        if (decodedVal == kInvInvalidValue)
            return kDIErrInvalidDiskByte;
        assert(decodedVal < sizeof(kDiskBytes53));

        chksum ^= decodedVal;
        threes[i] = chksum;
    }

    for (i = 0; i < 256; i++) {
        decodedVal = kInvDiskBytes53[buffer[idx++]];
        if (decodedVal == kInvInvalidValue)
            return kDIErrInvalidDiskByte;
        assert(decodedVal < sizeof(kDiskBytes53));

        chksum ^= decodedVal;
        base[i] = (chksum << 3);
    }

    /*
     * Grab the 411th byte (the checksum byte) and see if we did this
     * right.
     */
    //printf("Dec checksum value is 0x%02x\n", chksum);
    decodedVal = kInvDiskBytes53[buffer[idx++]];
    if (decodedVal == kInvInvalidValue)
        return kDIErrInvalidDiskByte;
    assert(decodedVal < sizeof(kDiskBytes53));
    chksum ^= decodedVal;

    if (pNibbleDescr->dataVerifyChecksum && chksum != 0) {
        WMSG1("    NIB bad data checksum (0x%02x)\n", chksum);
        return kDIErrBadChecksum;
    }

    /*
     * Convert this pile of stuff into 256 data bytes.
     */
    unsigned char* bufPtr;

    bufPtr = sctBuf;
    for (i = kChunkSize53-1; i >= 0; i--) {
        int three1, three2, three3, three4, three5;

        three1 = threes[i];
        three2 = threes[kChunkSize53 + i];
        three3 = threes[kChunkSize53*2 + i];
        three4 = (three1 & 0x02) << 1 | (three2 & 0x02) | (three3 & 0x02) >> 1;
        three5 = (three1 & 0x01) << 2 | (three2 & 0x01) << 1 | (three3 & 0x01);

        *bufPtr++ = base[i] | ((three1 >> 2) & 0x07);
        *bufPtr++ = base[kChunkSize53 + i] | ((three2 >> 2) & 0x07);
        *bufPtr++ = base[kChunkSize53*2 + i] | ((three3 >> 2) & 0x07);
        *bufPtr++ = base[kChunkSize53*3 + i] | (three4 & 0x07);
        *bufPtr++ = base[kChunkSize53*4 + i] | (three5 & 0x07);
    }
    assert(bufPtr == sctBuf + 255);

    /*
     * Convert the very last byte, which is handled specially.
     */
    *bufPtr = base[255] | (threes[kThreeSize-1] & 0x07);

    return kDIErrNone;
}

/*
 * Encode 5&3 encoding.
 */
void
DiskImg::EncodeNibble53(const CircularBufferAccess& buffer, int idx,
    const unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) const
{
    unsigned char top[kChunkSize53 * 5 +1];     // (255 / 0xff) +1
    unsigned char threes[kChunkSize53 * 3 +1];  // (153 / 0x99) +1
    int i, chunk;

    /*
     * Split the bytes into sections.
     */
    chunk = kChunkSize53-1;
    for (i = 0; i < (int) sizeof(top)-1; i += 5) {
        int three1, three2, three3, three4, three5;

        three1 = *sctBuf++;
        three2 = *sctBuf++;
        three3 = *sctBuf++;
        three4 = *sctBuf++;
        three5 = *sctBuf++;

        top[chunk] = three1 >> 3;
        top[chunk + kChunkSize53*1] = three2 >> 3;
        top[chunk + kChunkSize53*2] = three3 >> 3;
        top[chunk + kChunkSize53*3] = three4 >> 3;
        top[chunk + kChunkSize53*4] = three5 >> 3;

        threes[chunk] =
            (three1 & 0x07) << 2 | (three4 & 0x04) >> 1 | (three5 & 0x04) >> 2;
        threes[chunk + kChunkSize53*1] =
            (three2 & 0x07) << 2 | (three4 & 0x02) | (three5 & 0x02) >> 1;
        threes[chunk + kChunkSize53*2] =
            (three3 & 0x07) << 2 | (three4 & 0x01) << 1 | (three5 & 0x01);

        chunk--;
    }
    assert(chunk == -1);

    /*
     * Handle the last byte.
     */
    int val;
    val = *sctBuf++;
    top[255] = val >> 3;
    threes[kThreeSize-1] = val & 0x07;

    /*
     * Write the bytes.
     */
    int chksum = pNibbleDescr->dataChecksumSeed;
    for (i = sizeof(threes)-1; i >= 0; i--) {
        assert(threes[i] < sizeof(kDiskBytes53));
        buffer[idx++] = kDiskBytes53[threes[i] ^ chksum];
        chksum = threes[i];
    }

    for (i = 0; i < 256; i++) {
        assert(top[i] < sizeof(kDiskBytes53));
        buffer[idx++] = kDiskBytes53[top[i] ^ chksum];
        chksum = top[i];
    }

    //printf("Enc checksum value is 0x%02x\n", chksum);
    buffer[idx++] = kDiskBytes53[chksum];
}


/*
 * ===========================================================================
 *      Higher-level functions
 * ===========================================================================
 */

/*
 * Dump some bytes as hex values into a string.
 *
 * "buf" must be able to hold (num * 3) characters.
 */
static void
DumpBytes(const unsigned char* bytes, unsigned int num, char* buf)
{
    sprintf(buf, "%02x", bytes[0]);
    buf += 2;

    for (int i = 1; i < (int) num; i++) {
        sprintf(buf, " %02x", bytes[i]);
        buf += 3;
    }

    *buf = '\0';
}

static inline const char*
VerifyStr(bool val)
{
    return val ? "verify" : "ignore";
}

/*
 * Dump the contents of a NibbleDescr struct.
 */
void
DiskImg::DumpNibbleDescr(const NibbleDescr* pNibDescr) const
{
    char outBuf1[48];
    char outBuf2[48];
    const char* encodingStr;

    switch (pNibDescr->encoding) {
    case kNibbleEnc62:  encodingStr = "6&2";    break;
    case kNibbleEnc53:  encodingStr = "5&3";    break;
    case kNibbleEnc44:  encodingStr = "4&4";    break;
    default:            encodingStr = "???";    break;
    }

    WMSG1("NibbleDescr '%s':\n", pNibDescr->description);
    WMSG1("  Nibble encoding is %s\n", encodingStr);
    DumpBytes(pNibDescr->addrProlog, sizeof(pNibDescr->addrProlog), outBuf1);
    DumpBytes(pNibDescr->dataProlog, sizeof(pNibDescr->dataProlog), outBuf2);
    WMSG2("  Addr prolog: %s          Data prolog: %s\n", outBuf1, outBuf2);
    DumpBytes(pNibDescr->addrEpilog, sizeof(pNibDescr->addrEpilog), outBuf1);
    DumpBytes(pNibDescr->dataEpilog, sizeof(pNibDescr->dataEpilog), outBuf2);
    WMSG4("  Addr epilog: %s (%d)      Data epilog: %s (%d)\n",
        outBuf1, pNibDescr->addrEpilogVerifyCount,
        outBuf2, pNibDescr->dataEpilogVerifyCount);
    WMSG2("  Addr checksum: %s          Data checksum: %s\n",
        VerifyStr(pNibDescr->addrVerifyChecksum),
        VerifyStr(pNibDescr->dataVerifyChecksum));
    WMSG2("  Addr checksum seed: 0x%02x       Data checksum seed: 0x%02x\n",
        pNibDescr->addrChecksumSeed, pNibDescr->dataChecksumSeed);
    WMSG1("  Addr check track: %s\n",
        VerifyStr(pNibDescr->addrVerifyTrack));
}


/*
 * Load a nibble track into our track buffer.
 */
DIError
DiskImg::LoadNibbleTrack(long track, long* pTrackLen)
{
    DIError dierr = kDIErrNone;
	long offset;
    assert(track >= 0 && track < kMaxNibbleTracks525);

    *pTrackLen = GetNibbleTrackLength(track);
	offset = GetNibbleTrackOffset(track);
	assert(*pTrackLen > 0);
	assert(offset >= 0);

    if (track == fNibbleTrackLoaded) {
#ifdef NIB_VERBOSE_DEBUG
        WMSG1("  DI track %d already loaded\n", track);
#endif
        return kDIErrNone;
    } else {
        WMSG1("  DI loading track %ld\n", track);
    }

    /* invalidate in case we fail with partial read */
    fNibbleTrackLoaded = -1;

    /* alloc track buffer if needed */
    if (fNibbleTrackBuf == nil) {
        fNibbleTrackBuf = new unsigned char[kTrackAllocSize];
        if (fNibbleTrackBuf == nil)
            return kDIErrMalloc;
    }

    /*
     * Read the entire track into memory.
     */
    dierr = CopyBytesOut(fNibbleTrackBuf, offset, *pTrackLen);
    if (dierr != kDIErrNone)
        return dierr;

    fNibbleTrackLoaded = track;

	return dierr;
}

/*
 * Save the track buffer back to disk.
 */
DIError
DiskImg::SaveNibbleTrack(void)
{
    if (fNibbleTrackLoaded < 0) {
        WMSG0("ERROR: tried to save track without loading it first\n");
        return kDIErrInternal;
    }
    assert(fNibbleTrackBuf != nil);

    DIError dierr = kDIErrNone;
    long trackLen = GetNibbleTrackLength(fNibbleTrackLoaded);
    long offset = GetNibbleTrackOffset(fNibbleTrackLoaded);

    /* write the track to fpDataGFD */
    dierr = CopyBytesIn(fNibbleTrackBuf, offset, trackLen);
	return dierr;
}


/*
 * Count up the number of readable sectors found on this track, and
 * return it.  If "pVol" is non-nil, return the volume number from
 * one of the readable sectors.
 */
int
DiskImg::TestNibbleTrack(int track, const NibbleDescr* pNibbleDescr,
    int* pVol)
{
    long trackLen;
    int count = 0;

    assert(track >= 0 && track < kTrackCount525);
    assert(pNibbleDescr != nil);

    if (LoadNibbleTrack(track, &trackLen) != kDIErrNone) {
        WMSG0("   DI FindNibbleSectorStart: LoadNibbleTrack failed\n");
        return 0;
    }

    CircularBufferAccess buffer(fNibbleTrackBuf, trackLen);

    int i, sectorIdx;
    for (i = 0; i < pNibbleDescr->numSectors; i++) {
        int vol;
        sectorIdx = FindNibbleSectorStart(buffer, track, i, pNibbleDescr, &vol);
        if (sectorIdx >= 0) {
            if (pVol != nil)
                *pVol = vol;

            unsigned char sctBuf[256];
            if (DecodeNibbleData(buffer, sectorIdx, sctBuf, pNibbleDescr) == kDIErrNone)
                count++;
        }
    }

    WMSG3("   Tests on track=%d with '%s' returning count=%d\n",
        track, pNibbleDescr->description, count);

    return count;
}

/*
 * Analyze the nibblized track data.
 *
 * On entry:
 *  fPhysical indicates the appropriate nibble format
 *
 * On exit:
 *  fpNibbleDescr points to the most-likely-to-succeed NibbleDescr
 *  fDOSVolumeNum holds a volume number from one of the tracks
 *	fNumTracks holds the number of tracks on the disk
 */
DIError
DiskImg::AnalyzeNibbleData(void)
{
    assert(IsNibbleFormat(fPhysical));

	if (fPhysical == kPhysicalFormatNib525_Var) {
		/* TrackStar can have up to 40 */
		fNumTracks = fpImageWrapper->GetNibbleNumTracks();
		assert(fNumTracks > 0);
	} else {
		/* fixed-length formats (.nib, .nb2) are always 35 tracks */
		fNumTracks = kTrackCount525;
	}

    /*
     * Try to read sectors from tracks 1, 16, 17, and 26.  If we can get
     * at least 13 out of 16 (or 10 out of 13) on three out of four tracks,
     * we have a winner.
     */
    int i, good, goodTracks;
    int protoVol = kVolumeNumNotSet;

    for (i = 0; i < fNumNibbleDescrEntries; i++) {
		if (fpNibbleDescrTable[i].numSectors == 0) {
			/* uninitialized "custom" entry */
			WMSG1("  Skipping '%s'\n", fpNibbleDescrTable[i].description);
			continue;
		}
        WMSG1("  Trying '%s'\n", fpNibbleDescrTable[i].description);
        goodTracks = 0;

        good = TestNibbleTrack(1, &fpNibbleDescrTable[i], nil);
        if (good > fpNibbleDescrTable[i].numSectors - 4)
            goodTracks++;
        good = TestNibbleTrack(16, &fpNibbleDescrTable[i], nil);
        if (good > fpNibbleDescrTable[i].numSectors - 4)
            goodTracks++;
        good = TestNibbleTrack(17, &fpNibbleDescrTable[i], &protoVol);
        if (good > fpNibbleDescrTable[i].numSectors - 4)
            goodTracks++;
        good = TestNibbleTrack(26, &fpNibbleDescrTable[i], nil);
        if (good > fpNibbleDescrTable[i].numSectors - 4)
            goodTracks++;

        if (goodTracks >= 3) {
            WMSG3("  Looks like '%s' (%d-sector), vol=%d\n",
                fpNibbleDescrTable[i].description,
                fpNibbleDescrTable[i].numSectors, protoVol);
            fpNibbleDescr = &fpNibbleDescrTable[i];
            fDOSVolumeNum = protoVol;
            break;
        }
    }
    if (i == fNumNibbleDescrEntries) {
        WMSG0("AnalyzeNibbleData did not find matching NibbleDescr\n");
        return kDIErrBadNibbleSectors;
    }

    return kDIErrNone;
}


/*
 * Read a sector from a nibble image.
 *
 * While fNumTracks is valid, fNumSectPerTrack is a little flaky, because
 * in theory each track could be formatted differently.
 */
DIError
DiskImg::ReadNibbleSector(long track, int sector, void* buf,
    const NibbleDescr* pNibbleDescr)
{
	if (pNibbleDescr == nil) {
		/* disk has no recognizable sectors */
		WMSG0(" DI ReadNibbleSector: pNibbleDescr is nil, returning failure\n");
		return kDIErrBadNibbleSectors;
	}
	if (sector >= pNibbleDescr->numSectors) {
		/* e.g. trying to read sector 14 on a 13-sector disk */
		WMSG0(" DI ReadNibbleSector: bad sector number request\n");
		return kDIErrInvalidSector;
	}

    assert(pNibbleDescr != nil);
    assert(IsNibbleFormat(fPhysical));
    assert(track >= 0 && track < GetNumTracks());
    assert(sector >= 0 && sector < pNibbleDescr->numSectors);

    DIError dierr = kDIErrNone;
    long trackLen;
    int sectorIdx, vol;

    dierr = LoadNibbleTrack(track, &trackLen);
    if (dierr != kDIErrNone) {
        WMSG1("   DI ReadNibbleSector: LoadNibbleTrack %ld failed\n", track);
        return dierr;
    }

    CircularBufferAccess buffer(fNibbleTrackBuf, trackLen);
    sectorIdx = FindNibbleSectorStart(buffer, track, sector, pNibbleDescr,
                    &vol);
    if (sectorIdx < 0)
        return kDIErrSectorUnreadable;

    dierr = DecodeNibbleData(buffer, sectorIdx, (unsigned char*) buf,
                pNibbleDescr);

    return dierr;
}

/*
 * Write a sector to a nibble image.
 */
DIError
DiskImg::WriteNibbleSector(long track, int sector, const void* buf,
    const NibbleDescr* pNibbleDescr)
{
    assert(pNibbleDescr != nil);
    assert(IsNibbleFormat(fPhysical));
    assert(track >= 0 && track < GetNumTracks());
    assert(sector >= 0 && sector < pNibbleDescr->numSectors);
    assert(!fReadOnly);

    DIError dierr = kDIErrNone;
    long trackLen;
    int sectorIdx, vol;

    dierr = LoadNibbleTrack(track, &trackLen);
    if (dierr != kDIErrNone) {
        WMSG1("   DI ReadNibbleSector: LoadNibbleTrack %ld failed\n", track);
        return dierr;
    }

    CircularBufferAccess buffer(fNibbleTrackBuf, trackLen);
    sectorIdx = FindNibbleSectorStart(buffer, track, sector, pNibbleDescr,
                    &vol);
    if (sectorIdx < 0)
        return kDIErrSectorUnreadable;

    EncodeNibbleData(buffer, sectorIdx, (unsigned char*) buf, pNibbleDescr);

    dierr = SaveNibbleTrack();
    if (dierr != kDIErrNone) {
        WMSG1("   DI ReadNibbleSector: SaveNibbleTrack %ld failed\n", track);
        return dierr;
    }

    return dierr;
}


/*
 * Get the contents of the nibble track.
 *
 * "buf" must be able to hold kTrackAllocSize bytes.
 */
DIError
DiskImg::ReadNibbleTrack(long track, unsigned char* buf, long* pTrackLen)
{
	DIError dierr;

    dierr = LoadNibbleTrack(track, pTrackLen);
    if (dierr != kDIErrNone) {
        WMSG1("   DI ReadNibbleTrack: LoadNibbleTrack %ld failed\n", track);
        return dierr;
    }

	memcpy(buf, fNibbleTrackBuf, *pTrackLen);
	return kDIErrNone;
}

/*
 * Set the contents of a nibble track.
 *
 * NOTE: This currently does the wrong thing when converting from .nb2 to
 * .nib.  Fixed-length formats shouldn't be allowed to interact.  Figure
 * this out someday.  For now, the higher-level code prevents it.
 */
DIError
DiskImg::WriteNibbleTrack(long track, const unsigned char* buf, long trackLen)
{
	DIError dierr;
	long oldTrackLen;

	/* load the track to set the "current track" stuff */
    dierr = LoadNibbleTrack(track, &oldTrackLen);
    if (dierr != kDIErrNone) {
        WMSG1("   DI WriteNibbleTrack: LoadNibbleTrack %ld failed\n", track);
        return dierr;
    }

	if (trackLen > GetNibbleTrackAllocLength()) {
		WMSG2("ERROR: tried to write too-long track len (%ld vs %d)\n",
			trackLen, GetNibbleTrackAllocLength());
		return kDIErrInvalidArg;
	}

	if (trackLen < oldTrackLen)		// pad out any extra space
		memset(fNibbleTrackBuf, 0xff, oldTrackLen);
	memcpy(fNibbleTrackBuf, buf, trackLen);
	fpImageWrapper->SetNibbleTrackLength(track, trackLen);

    dierr = SaveNibbleTrack();
    if (dierr != kDIErrNone) {
        WMSG1("   DI ReadNibbleSector: SaveNibbleTrack %ld failed\n", track);
        return dierr;
    }

	return kDIErrNone;
}


/*
 * Create a blank nibble image, using fpNibbleDescr as the template.
 * Sets "fLength".
 *
 * Tracks are written the same way regardless of actual track length (be
 * it 6656, 6384, or variable-length).  Anything longer than 6384 just has
 * more padding at the end of the track.
 *
 * The format looks like this:
 *  Gap one (48 self-sync bytes)
 *  For each sector:
 *   Address field (14 bytes, e.g. d5aa96 vol track sect chksum deaaeb)
 *   Gap two (six self-sync bytes)
 *   Data field (6 header bytes, 1 checksum byte, and 342 or 410 data bytes)
 *   Gap three (27 self-sync bytes)
 *
 * 48 + (14 + 6 + (6 + 1 + 342) + 27) * 16 = 6384
 * 48 + (14 + 6 + (6 + 1 + 410) + 27) * 13 = 6080
 */
DIError
DiskImg::FormatNibbles(GenericFD* pGFD) const
{
    assert(fHasNibbles);
    assert(GetNumTracks() > 0);

    DIError dierr = kDIErrNone;
    unsigned char trackBuf[kTrackAllocSize];
	/* these should be the same except for var-len images */
	long trackAllocLen = GetNibbleTrackAllocLength();
    long trackLen = GetNibbleTrackFormatLength();
    int track;

    assert(trackLen > 0);
    pGFD->Rewind();

	/*
	 * If we don't have sector access, take a shortcut and just fill the
	 * entire image with 0xff.
	 */
	if (!fHasSectors) {
		memset(trackBuf, 0xff, trackLen);
		for (track = 0; track < GetNumTracks(); track++) {
			/* write the track to the GFD */
			dierr = pGFD->Write(trackBuf, trackAllocLen);
			if (dierr != kDIErrNone)
				return dierr;
			fpImageWrapper->SetNibbleTrackLength(track, trackAllocLen);
		}

		return kDIErrNone;
	}


    assert(fHasSectors);
    assert(fpNibbleDescr != nil);
    assert(fpNibbleDescr->numSectors == GetNumSectPerTrack());
    assert(fpNibbleDescr->encoding == kNibbleEnc53 ||
           fpNibbleDescr->encoding == kNibbleEnc62);
    assert(fDOSVolumeNum != kVolumeNumNotSet);

    /*
     * Create a prototype sector.  The data for a sector full of zeroes
     * is exactly the same; only the address header changes.
     */
    unsigned char sampleSource[256];
    unsigned char sampleBuf[512];      // must hold 5&3 and 6&2
    CircularBufferAccess sample(sampleBuf, 512);
    long dataLen;

    if (fpNibbleDescr->encoding == kNibbleEnc53)
        dataLen = 410 +1;
    else
        dataLen = 342 +1;

    memset(sampleSource, 0, sizeof(sampleSource));
    EncodeNibbleData(sample, 0, sampleSource, fpNibbleDescr);

    /*
     * For each track in the image, "format" the expected number of
     * sectors, then write the data to the GFD.
     */
    for (track = 0; track < GetNumTracks(); track++) {
        //WMSG1("Formatting track %d\n", track);
        unsigned char* trackPtr = trackBuf;

        /*
         * Fill with "self-sync" bytes.
         */
        memset(trackBuf, 0xff, trackAllocLen);

        /* gap one */
        trackPtr += 48;

        for (int sector = 0; sector < fpNibbleDescr->numSectors; sector++) {
            /*
             * Write address field.
             */
            unsigned short hdrTrack, hdrSector, hdrVol, hdrChksum;
            hdrTrack = track;
            hdrSector = sector;
            hdrVol = fDOSVolumeNum;
            *trackPtr++ = fpNibbleDescr->addrProlog[0];
            *trackPtr++ = fpNibbleDescr->addrProlog[1];
            *trackPtr++ = fpNibbleDescr->addrProlog[2];
            *trackPtr++ = Conv44(hdrVol, true);
            *trackPtr++ = Conv44(hdrVol, false);
            *trackPtr++ = Conv44(hdrTrack, true);
            *trackPtr++ = Conv44(hdrTrack, false);
            *trackPtr++ = Conv44(hdrSector, true);
            *trackPtr++ = Conv44(hdrSector, false);
            hdrChksum = fpNibbleDescr->addrChecksumSeed ^
                            hdrVol ^ hdrTrack ^ hdrSector;
            *trackPtr++ = Conv44(hdrChksum, true);
            *trackPtr++ = Conv44(hdrChksum, false);
            *trackPtr++ = fpNibbleDescr->addrEpilog[0];
            *trackPtr++ = fpNibbleDescr->addrEpilog[1];
            *trackPtr++ = fpNibbleDescr->addrEpilog[2];

            /* gap two */
            trackPtr += 6;

            /*
             * Write data field.
             */
            *trackPtr++ = fpNibbleDescr->dataProlog[0];
            *trackPtr++ = fpNibbleDescr->dataProlog[1];
            *trackPtr++ = fpNibbleDescr->dataProlog[2];
            memcpy(trackPtr, sampleBuf, dataLen);
            trackPtr += dataLen;
            *trackPtr++ = fpNibbleDescr->dataEpilog[0];
            *trackPtr++ = fpNibbleDescr->dataEpilog[1];
            *trackPtr++ = fpNibbleDescr->dataEpilog[2];

            /* gap three */
            trackPtr += 27;
        }

        assert(trackPtr - trackBuf == 6384 ||
               trackPtr - trackBuf == 6080);

        /*
         * Write the track to the GFD.
         */
        dierr = pGFD->Write(trackBuf, trackAllocLen);
        if (dierr != kDIErrNone)
            break;

		/* on a variable-length image, reduce track len to match */
		fpImageWrapper->SetNibbleTrackLength(track, trackLen);
    }

    return dierr;
}

