/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * Disk editor implementation.
 *
 * Note to self: it should be possible to open an image in "nibble" mode,
 * switch to one of the various "sector" modes, and maybe even try out a
 * "block" mode for a while.  Locking the editor into one particular mode
 * makes for a cumbersome interface when dealing with certain kinds of disks.
 * It should also be possible to configure the "custom" NibbleDescr and then
 * re-analyze the disk, possibly transferring the customization over to
 * the main file listing.  (Perhaps the customization affects a global slot?
 * Probably want more than one set of custom nibble values, with the config
 * dialog accessible from main menu and from within disk editor.)
 *
 * A track copier would be neat too.
 */
#include "stdafx.h"
#include "SubVolumeDialog.h"
#include "DEFileDialog.h"
#include "DiskEditDialog.h"


/*
 * ===========================================================================
 *      DiskEditDialog (base class)
 * ===========================================================================
 */

BEGIN_MESSAGE_MAP(DiskEditDialog, CDialog)
    ON_BN_CLICKED(IDC_DISKEDIT_DONE, OnDone)
    ON_BN_CLICKED(IDC_DISKEDIT_HEX, OnHexMode)
    ON_BN_CLICKED(IDC_DISKEDIT_DOREAD, OnDoRead)
    ON_BN_CLICKED(IDC_DISKEDIT_DOWRITE, OnDoWrite)
    ON_BN_CLICKED(IDC_DISKEDIT_PREV, OnReadPrev)
    ON_BN_CLICKED(IDC_DISKEDIT_NEXT, OnReadNext)
    ON_BN_CLICKED(IDC_DISKEDIT_SUBVOLUME, OnSubVolume)
    ON_BN_CLICKED(IDC_DISKEDIT_OPENFILE, OnOpenFile)
    ON_BN_CLICKED(IDHELP, OnHelp)
    ON_CBN_SELCHANGE(IDC_DISKEDIT_NIBBLE_PARMS, OnNibbleParms)
    ON_WM_HELPINFO()
END_MESSAGE_MAP()

BOOL DiskEditDialog::OnInitDialog(void)
{
    ASSERT(!fFileName.IsEmpty());
    ASSERT(fpDiskFS != NULL);

    /*
     * Disable the write button.
     */
    if (fReadOnly) {
        CButton* pButton = (CButton*) GetDlgItem(IDC_DISKEDIT_DOWRITE);
        ASSERT(pButton != NULL);
        pButton->EnableWindow(FALSE);
    }

    /*
     * Use modified spin controls so we're not limited to 16 bits.
     */
    ReplaceSpinCtrl(&fTrackSpinner, IDC_DISKEDIT_TRACKSPIN,
        IDC_DISKEDIT_TRACK);
    ReplaceSpinCtrl(&fSectorSpinner, IDC_DISKEDIT_SECTORSPIN,
        IDC_DISKEDIT_SECTOR);



    /*
     * Configure the RichEdit control.
     */
    CRichEditCtrl* pEdit = (CRichEditCtrl*) GetDlgItem(IDC_DISKEDIT_EDIT);
    ASSERT(pEdit != NULL);

    /* set the font to 10-point Courier New */
    CHARFORMAT cf;
    cf.cbSize = sizeof(CHARFORMAT);
    cf.dwMask = CFM_FACE | CFM_SIZE;
    wcscpy(cf.szFaceName, L"Courier New");
    cf.yHeight = 10 * 20;       // point size in twips
    BOOL cc = pEdit->SetDefaultCharFormat(cf);
    if (cc == FALSE) {
        LOGI("SetDefaultCharFormat failed?");
        ASSERT(FALSE);
    }

    /* set to read-only */
    pEdit->SetReadOnly();
    /* retain the selection even if we lose focus [can't do this in OnInit] */
    pEdit->SetOptions(ECOOP_OR, ECO_SAVESEL);

    /*
     * Disable the sub-volume and/or file open buttons if the DiskFS doesn't
     * have the appropriate stuff inside.
     */
    if (fpDiskFS->GetNextFile(NULL) == NULL) {
        CWnd* pWnd = GetDlgItem(IDC_DISKEDIT_OPENFILE);
        pWnd->EnableWindow(FALSE);
    }
    if (fpDiskFS->GetNextSubVolume(NULL) == NULL) {
        CWnd* pWnd = GetDlgItem(IDC_DISKEDIT_SUBVOLUME);
        pWnd->EnableWindow(FALSE);
    }

    /*
     * Configure the nibble parm drop-list in an appropriate fashion.
     */
    InitNibbleParmList();

    /*
     * If this is a sub-volume edit window, pop us up slightly offset from the
     * parent window so the user can see that there's more than one thing
     * open.
     */
    if (fPositionShift != 0) {
        CRect rect;
        GetWindowRect(&rect);
        rect.top += fPositionShift;
        rect.left += fPositionShift;
        rect.bottom += fPositionShift;
        rect.right += fPositionShift;
        MoveWindow(&rect);
    }

    /*
     * Set the window title.
     */
    CString title("Disk Viewer - ");
    title += fFileName;
    if (fpDiskFS->GetVolumeID() != NULL) {
        title += " (";
        title += fpDiskFS->GetVolumeID();
        title += ")";
    }
    SetWindowText(title);

    return TRUE;
}

void DiskEditDialog::InitNibbleParmList(void)
{
    ASSERT(fpDiskFS != NULL);
    DiskImg* pDiskImg = fpDiskFS->GetDiskImg();
    CComboBox* pCombo;

    pCombo = (CComboBox*) GetDlgItem(IDC_DISKEDIT_NIBBLE_PARMS);
    ASSERT(pCombo != NULL);

    if (pDiskImg->GetHasNibbles()) {
        const DiskImg::NibbleDescr* pTable;
        const DiskImg::NibbleDescr* pCurrent;
        int i, count;

        pTable = pDiskImg->GetNibbleDescrTable(&count);
        if (pTable == NULL || count <= 0) {
            LOGI("WHOOPS: nibble parm got table=0x%08lx, count=%d",
                (long) pTable, count);
            return;
        }
        pCurrent = pDiskImg->GetNibbleDescr();

        /* configure the selection to match the disk analysis */
        int dflt = -1;
        if (pCurrent != NULL) {
            for (i = 0; i < count; i++) {
                if (memcmp(&pTable[i], pCurrent, sizeof(*pCurrent)) == 0) {
                    LOGI("  NibbleParm match on entry %d", i);
                    dflt = i;
                    break;
                }
            }

            if (dflt == -1) {
                LOGI("  GLITCH: no match on nibble descr in table?!");
                dflt = 0;
            }
        }

        for (i = 0; i < count; i++) {
            if (pTable[i].numSectors > 0) {
                CString description(pTable[i].description);
                pCombo->AddString(description);
            } else {
                /* only expecting this on the last, "custom" entry */
                ASSERT(i == count-1);
            }
        }
        pCombo->SetCurSel(dflt);
    } else {
        pCombo->AddString(L"Nibble Parms");
        pCombo->SetCurSel(0);
        pCombo->EnableWindow(FALSE);
    }
}

int DiskEditDialog::ReplaceSpinCtrl(MySpinCtrl* pNewSpin, int idSpin, int idEdit)
{
    CSpinButtonCtrl* pSpin;
//  CRect rect;
    DWORD style;
        
    pSpin = (CSpinButtonCtrl*)GetDlgItem(idSpin);
    if (pSpin == NULL)
        return -1;
//  pSpin->GetWindowRect(&rect);
//  ScreenToClient(&rect);
    style = pSpin->GetStyle();
    style &= ~(UDS_SETBUDDYINT);
    //style |= UDS_AUTOBUDDY;
    ASSERT(!(style & UDS_AUTOBUDDY));
    pSpin->DestroyWindow();
    pNewSpin->Create(style, CRect(0,0,0,0), this, idSpin);
    pNewSpin->SetBuddy(GetDlgItem(idEdit));

    return 0;
}

BOOL DiskEditDialog::PreTranslateMessage(MSG* pMsg)
{
    if (pMsg->message == WM_KEYDOWN &&
         pMsg->wParam == VK_RETURN)
    {
        //LOGI("RETURN!");
        LoadData();
        return TRUE;
    }

    return CDialog::PreTranslateMessage(pMsg);
}

void DiskEditDialog::OnDone(void)
{
    LOGI("DiskEditDialog OnDone");
    EndDialog(IDOK);
}

void DiskEditDialog::OnHexMode(void)
{
    int base;

    CButton* pButton = (CButton*) GetDlgItem(IDC_DISKEDIT_HEX);
    ASSERT(pButton != NULL);

    if (pButton->GetCheck() == 0)
        base = 10;
    else
        base = 16;

    SetSpinMode(IDC_DISKEDIT_TRACKSPIN, base);
    SetSpinMode(IDC_DISKEDIT_SECTORSPIN, base);
}

void DiskEditDialog::OnSubVolume(void)
{
    SubVolumeDialog subv(this);
    bool showAsBlocks;

    subv.Setup(fpDiskFS);
    if (subv.DoModal() == IDOK) {
        LOGI("SELECTED subv %d", subv.fListBoxIndex);
        DiskFS::SubVolume* pSubVol = fpDiskFS->GetNextSubVolume(NULL);
        if (pSubVol == NULL)
            return;

        while (subv.fListBoxIndex-- > 0) {
            pSubVol = fpDiskFS->GetNextSubVolume(pSubVol);
        }
        ASSERT(pSubVol != NULL);

        BlockEditDialog blockEdit;
        SectorEditDialog sectorEdit;
        DiskEditDialog* pEditDialog;

        showAsBlocks = pSubVol->GetDiskImg()->ShowAsBlocks();
        if (showAsBlocks)
            pEditDialog = &blockEdit;
        else
            pEditDialog = &sectorEdit;
        CString volumeID(fpDiskFS->GetVolumeID());
        pEditDialog->Setup(pSubVol->GetDiskFS(), volumeID);
        pEditDialog->SetPositionShift(8);
        (void) pEditDialog->DoModal();
    }
}

void DiskEditDialog::SetSpinMode(int id, int base)
{
    CString valStr;

    ASSERT(base == 10 || base == 16);

    MySpinCtrl* pSpin = (MySpinCtrl*) GetDlgItem(id);
    if (pSpin == NULL) {
        // expected behavior in "block" mode for sector button
        LOGI("Couldn't find spin button %d", id);
        return;
    }

    long val = pSpin->GetPos();
    if (val & 0xff000000) {
        LOGI("NOTE: hex transition while value is invalid");
        val = 0;
    }

    if (base == 10)
        valStr.Format(L"%d", val);
    else
        valStr.Format(L"%X", val);

    pSpin->SetBase(base);
    pSpin->GetBuddy()->SetWindowText(valStr);
    LOGI("Set spin button base to %d val=%d", base, val);
}

int DiskEditDialog::ReadSpinner(int id, long* pVal)
{
    MySpinCtrl* pSpin = (MySpinCtrl*) GetDlgItem(id);
    ASSERT(pSpin != NULL);

    long val = pSpin->GetPos();
    if (val & 0xff000000) {
        /* error */
        CString msg, err;

        CheckedLoadString(&err, IDS_ERROR);
        int lower, upper;
        pSpin->GetRange32(lower, upper);
        msg.Format(L"Please enter a value between %d and %d (0x%x and 0x%x).",
            lower, upper, lower, upper);
        MessageBox(msg, err, MB_OK|MB_ICONEXCLAMATION);
        return -1;
    }

    *pVal = val;
    return 0;
}

void DiskEditDialog::SetSpinner(int id, long val)
{
    MySpinCtrl* pSpin = (MySpinCtrl*) GetDlgItem(id);
    ASSERT(pSpin != NULL);

    /* sanity check */
    int lower, upper;
    pSpin->GetRange32(lower, upper);
    ASSERT(val >= lower && val <= upper);

    pSpin->SetPos(val);
}

void DiskEditDialog::DisplayData(const uint8_t* srcBuf, int size)
{
    WCHAR textBuf[80 * 16 * 2];
    WCHAR* cp;
    int i, j;

    ASSERT(srcBuf != NULL);
    ASSERT(size == kSectorSize || size == kBlockSize);

    CRichEditCtrl* pEdit = (CRichEditCtrl*)GetDlgItem(IDC_DISKEDIT_EDIT);
    ASSERT(pEdit != NULL);

    /*
     * If we have an alert message, show that instead.
     */
    if (!fAlertMsg.IsEmpty()) {
        const int kWidth = 72;
        int indent = (kWidth/2) - (fAlertMsg.GetLength() / 2);
        if (indent < 0)
            indent = 0;

        CString msg = L"                                    "
                      L"                                    ";
        ASSERT(msg.GetLength() == kWidth);
        msg = msg.Left(indent);
        msg += fAlertMsg;
        for (i = 0; i < (size / 16)-2; i += 2) {
            textBuf[i] = '\r';
            textBuf[i+1] = '\n';
        }
        wcscpy(&textBuf[i], msg);
        pEdit->SetWindowText(textBuf);

        return;
    }

    /*
     * No alert, do the usual thing.
     */
    cp = textBuf;
    for (i = 0; i < size/16; i++) {
        if (size == kSectorSize) {
            /* two-nybble addr */
            wsprintf(cp, L" %02x: %02x %02x %02x %02x %02x %02x %02x %02x "
                         L"%02x %02x %02x %02x %02x %02x %02x %02x  ",
                i * 16,
                srcBuf[0], srcBuf[1], srcBuf[2], srcBuf[3],
                srcBuf[4], srcBuf[5], srcBuf[6], srcBuf[7],
                srcBuf[8], srcBuf[9], srcBuf[10], srcBuf[11],
                srcBuf[12], srcBuf[13], srcBuf[14], srcBuf[15]);
        } else {
            /* three-nybble addr */
            wsprintf(cp, L"%03x: %02x %02x %02x %02x %02x %02x %02x %02x "
                         L"%02x %02x %02x %02x %02x %02x %02x %02x  ",
                i * 16,
                srcBuf[0], srcBuf[1], srcBuf[2], srcBuf[3],
                srcBuf[4], srcBuf[5], srcBuf[6], srcBuf[7],
                srcBuf[8], srcBuf[9], srcBuf[10], srcBuf[11],
                srcBuf[12], srcBuf[13], srcBuf[14], srcBuf[15]);
        }
        ASSERT(wcslen(cp) == 54);
        cp += 54;       // strlen(cp)
        for (j = 0; j < 16; j++)
            *cp++ = PrintableChar(srcBuf[j]);

        *cp++ = '\r';
        *cp++ = '\n';

        srcBuf += 16;
    }
    /* kill the last EOL, so the cursor doesn't move past that line */
    cp--;
    *cp = '\0';

    pEdit->SetWindowText(textBuf);
}

void DiskEditDialog::DisplayNibbleData(const unsigned char* srcBuf, int size)
{
    ASSERT(srcBuf != NULL);
    ASSERT(size > 0);
    ASSERT(fAlertMsg.IsEmpty());

    int bufSize = ((size+15) / 16) * 80;
    WCHAR* textBuf = new WCHAR[bufSize];
    WCHAR* cp;
    int i;

    if (textBuf == NULL)
        return;

    cp = textBuf;
    for (i = 0; size > 0; i++) {
        if (size >= 16) {
            wsprintf(cp, L"%04x: %02x %02x %02x %02x %02x %02x %02x %02x "
                         L"%02x %02x %02x %02x %02x %02x %02x %02x",
                i * 16,
                srcBuf[0], srcBuf[1], srcBuf[2], srcBuf[3],
                srcBuf[4], srcBuf[5], srcBuf[6], srcBuf[7],
                srcBuf[8], srcBuf[9], srcBuf[10], srcBuf[11],
                srcBuf[12], srcBuf[13], srcBuf[14], srcBuf[15]);
            ASSERT(wcslen(cp) == 53);
            cp += 53;       // strlen(cp)
        } else {
            wsprintf(cp, L"%04x:", i * 16);
            cp += 5;
            for (int j = 0; j < size; j++) {
                wsprintf(cp, L" %02x", srcBuf[j]);
                cp += 3;
            }
        }

        *cp++ = '\r';
        *cp++ = '\n';

        srcBuf += 16;
        size -= 16;
    }
    /* kill the last EOL, so the cursor doesn't move past that line */
    cp--;
    *cp = '\0';

    CRichEditCtrl* pEdit = (CRichEditCtrl*)GetDlgItem(IDC_DISKEDIT_EDIT);
    ASSERT(pEdit != NULL);
    pEdit->SetWindowText(textBuf);

    /*
     * Handle resize of edit box.  We have to do this late or the scroll bar
     * won't appear under Win98.  (Whatever.)
     */
    if (fFirstResize) {
        fFirstResize = false;

        const int kStretchHeight = 249;
        CRect rect;

        GetWindowRect(&rect);

        CRect inner;
        pEdit->GetRect(&inner);
        inner.bottom += kStretchHeight;
        pEdit->GetWindowRect(&rect);
        ScreenToClient(&rect);
        rect.bottom += kStretchHeight;
        pEdit->MoveWindow(&rect);
        pEdit->SetRect(&inner);
    }

    delete[] textBuf;
}

DIError DiskEditDialog::OpenFile(const WCHAR* fileName, bool openRsrc,
    A2File** ppFile, A2FileDescr** ppOpenFile)
{
    A2File* pFile;
    A2FileDescr* pOpenFile = NULL;

    LOGI(" OpenFile '%ls' rsrc=%d", fileName, openRsrc);
    CStringA fileNameA(fileName);
    pFile = fpDiskFS->GetFileByName(fileNameA);
    if (pFile == NULL) {
        CString msg, failed;

        msg.Format(IDS_DEFILE_FIND_FAILED, fileName);
        CheckedLoadString(&failed, IDS_FAILED);
        MessageBox(msg, failed, MB_OK | MB_ICONSTOP);
        return kDIErrFileNotFound;
    }

    DIError dierr;
    dierr = pFile->Open(&pOpenFile, true, openRsrc);
    if (dierr != kDIErrNone) {
        CString msg, failed;

        msg.Format(IDS_DEFILE_OPEN_FAILED, fileName,
            DiskImgLib::DIStrError(dierr));
        CheckedLoadString(&failed, IDS_FAILED);
        MessageBox(msg, failed, MB_OK | MB_ICONSTOP);
        return dierr;
    }

    *ppFile = pFile;
    *ppOpenFile = pOpenFile;

    return kDIErrNone;
}

void DiskEditDialog::OnNibbleParms(void)
{
    DiskImg* pDiskImg = fpDiskFS->GetDiskImg();
    CComboBox* pCombo;
    int sel;

    ASSERT(pDiskImg != NULL);
    ASSERT(pDiskImg->GetHasNibbles());

    pCombo = (CComboBox*) GetDlgItem(IDC_DISKEDIT_NIBBLE_PARMS);
    ASSERT(pCombo != NULL);

    sel = pCombo->GetCurSel();
    if (sel == CB_ERR)
        return;

    LOGI(" OnNibbleParms: entry %d now selected", sel);
    const DiskImg::NibbleDescr* pNibbleTable;
    int count;
    pNibbleTable = pDiskImg->GetNibbleDescrTable(&count);
    ASSERT(sel < count);
    pDiskImg->SetNibbleDescr(sel);

    LoadData();
}


#if 0
/*
 * Make a "sparse" block in a file obvious by filling it with the word
 * "sparse".
 */
void
DiskEditDialog::FillWithPattern(unsigned char* buf, int size,
    const char* pattern)
{
    const char* cp;
    unsigned char* ucp;

    ucp = buf;
    cp = pattern;
    while (ucp < buf+size) {
        *ucp++ = *cp++;
        if (*cp == '\0')
            cp = pattern;
    }
}
#endif


/*
 * ===========================================================================
 *      SectorEditDialog
 * ===========================================================================
 */

BOOL SectorEditDialog::OnInitDialog(void)
{
    /*
     * Do base-class construction.
     */
    DiskEditDialog::OnInitDialog();

    /*
     * Change track/sector text.
     */
    CString trackStr;
    CWnd* pWnd;
    trackStr.Format(L"Track (%d):", fpDiskFS->GetDiskImg()->GetNumTracks());
    pWnd = GetDlgItem(IDC_STEXT_TRACK);
    ASSERT(pWnd != NULL);
    pWnd->SetWindowText(trackStr);
    trackStr.Format(L"Sector (%d):", fpDiskFS->GetDiskImg()->GetNumSectPerTrack());
    pWnd = GetDlgItem(IDC_STEXT_SECTOR);
    ASSERT(pWnd != NULL);
    pWnd->SetWindowText(trackStr);

    /*
     * Configure the spin buttons.
     */
    MySpinCtrl* pSpin;
    pSpin = (MySpinCtrl*)GetDlgItem(IDC_DISKEDIT_TRACKSPIN);
    ASSERT(pSpin != NULL);
    pSpin->SetRange32(0, fpDiskFS->GetDiskImg()->GetNumTracks()-1);
    pSpin->SetPos(0);
    pSpin = (MySpinCtrl*)GetDlgItem(IDC_DISKEDIT_SECTORSPIN);
    ASSERT(pSpin != NULL);
    pSpin->SetRange32(0, fpDiskFS->GetDiskImg()->GetNumSectPerTrack()-1);
    pSpin->SetPos(0);

    /* give us something to look at */
    LoadData();

    return TRUE;
}

int SectorEditDialog::LoadData(void)
{
    //LOGI("SED LoadData");
    ASSERT(fpDiskFS != NULL);
    ASSERT(fpDiskFS->GetDiskImg() != NULL);

    if (ReadSpinner(IDC_DISKEDIT_TRACKSPIN, &fTrack) != 0)
        return -1;
    if (ReadSpinner(IDC_DISKEDIT_SECTORSPIN, &fSector) != 0)
        return -1;

    LOGI("LoadData reading t=%d s=%d", fTrack, fSector);

    fAlertMsg = "";
    DIError dierr;
    dierr = fpDiskFS->GetDiskImg()->ReadTrackSector(fTrack, fSector, fSectorData);
    if (dierr != kDIErrNone) {
        LOGI("SED sector read failed: %hs", DiskImgLib::DIStrError(dierr));
        //CString msg;
        //CString err;
        //err.LoadString(IDS_ERROR);
        //msg.Format(IDS_DISKEDIT_NOREADTS, fTrack, fSector);
        //MessageBox(msg, err, MB_OK|MB_ICONSTOP);
        CheckedLoadString(&fAlertMsg, IDS_DISKEDITMSG_BADSECTOR);
        //return -1;
    }

    DisplayData();

    return 0;
}

void SectorEditDialog::OnDoRead(void)
{
    LoadData();
}

void SectorEditDialog::OnDoWrite(void)
{
    MessageBox(L"Write!");
}

void SectorEditDialog::OnReadPrev(void)
{
    if (fTrack == 0 && fSector == 0)
        return;

    if (fSector == 0) {
        fSector = fpDiskFS->GetDiskImg()->GetNumSectPerTrack() -1;
        fTrack--;
    } else {
        fSector--;
    }

    SetSpinner(IDC_DISKEDIT_TRACKSPIN, fTrack);
    SetSpinner(IDC_DISKEDIT_SECTORSPIN, fSector);

    LoadData();
}

void SectorEditDialog::OnReadNext(void)
{
    int numTracks = fpDiskFS->GetDiskImg()->GetNumTracks();
    int numSects = fpDiskFS->GetDiskImg()->GetNumSectPerTrack();

    if (fTrack == numTracks-1 && fSector == numSects-1)
        return;

    if (fSector == numSects-1) {
        fSector = 0;
        fTrack++;
    } else {
        fSector++;
    }

    SetSpinner(IDC_DISKEDIT_TRACKSPIN, fTrack);
    SetSpinner(IDC_DISKEDIT_SECTORSPIN, fSector);

    LoadData();
}

void SectorEditDialog::OnOpenFile(void)
{
    DEFileDialog fileDialog(this);

    if (fileDialog.DoModal() == IDOK) {
        SectorFileEditDialog fileEdit(this, this);
        A2File* pFile;
        A2FileDescr* pOpenFile = NULL;
        DIError dierr;

        dierr = OpenFile(fileDialog.fName, fileDialog.fOpenRsrcFork != 0,
                    &pFile, &pOpenFile);
        if (dierr != kDIErrNone)
            return;

        fileEdit.SetupFile(fileDialog.fName, fileDialog.fOpenRsrcFork != 0,
            pFile, pOpenFile);
        fileEdit.SetPositionShift(8);
        (void) fileEdit.DoModal();

        pOpenFile->Close();
    }
}


/*
 * ===========================================================================
 *      SectorFileEditDialog
 * ===========================================================================
 */

BOOL SectorFileEditDialog::OnInitDialog(void)
{
    BOOL retval;

    /* do base class first */
    retval = SectorEditDialog::OnInitDialog();

    /* disable direct entry of tracks and sectors */
    CWnd* pWnd;
    pWnd = GetDlgItem(IDC_DISKEDIT_TRACKSPIN);
    ASSERT(pWnd != NULL);
    pWnd->EnableWindow(FALSE);
    pWnd = GetDlgItem(IDC_DISKEDIT_SECTORSPIN);
    ASSERT(pWnd != NULL);
    pWnd->EnableWindow(FALSE);

    /* disallow opening of sub-volumes and files */
    pWnd = GetDlgItem(IDC_DISKEDIT_OPENFILE);
    pWnd->EnableWindow(FALSE);
    pWnd = GetDlgItem(IDC_DISKEDIT_SUBVOLUME);
    pWnd->EnableWindow(FALSE);

    CEdit* pEdit;
    pEdit = (CEdit*) GetDlgItem(IDC_DISKEDIT_TRACK);
    ASSERT(pEdit != NULL);
    pEdit->SetReadOnly(TRUE);
    pEdit = (CEdit*) GetDlgItem(IDC_DISKEDIT_SECTOR);
    ASSERT(pEdit != NULL);
    pEdit->SetReadOnly(TRUE);

    /* set the window title */
    CString title;
    CString rsrcIndic;
    CheckedLoadString(&rsrcIndic, IDS_INDIC_RSRC);
    title.Format(L"Disk Viewer - %hs%ls (%I64d bytes)",
        (LPCSTR) fpFile->GetPathName(),  // use fpFile version to get case
        fOpenRsrcFork ? (LPCWSTR)rsrcIndic : L"", (LONGLONG) fLength);
    SetWindowText(title);

    return retval;
}

int SectorFileEditDialog::LoadData(void)
{
    ASSERT(fpDiskFS != NULL);
    ASSERT(fpDiskFS->GetDiskImg() != NULL);

    DIError dierr;
    LOGI("SFED LoadData reading index=%d", fSectorIdx);

#if 0
    LOGI("LoadData reading offset=%d", fOffset);
    size_t actual = 0;
    dierr = fpFile->Seek(fOffset, EmbeddedFD::kSeekSet);
    if (dierr == kDIErrNone) {
        dierr = fpFile->Read(fSectorData, 1 /*kSectorSize*/, &actual);
    }
    if (dierr != kDIErrNone) {
        CString msg, failed;
        failed.LoadString(IDS_FAILED);
        msg.Format(IDS_DISKEDIT_FIRDFAILED, DiskImg::DIStrError(dierr));
        MessageBox(msg, failed, MB_OK);
        // TO DO: mark contents as invalid, so editing fails
        return -1;
    }

    if (actual != kSectorSize) {
        LOGI(" SFED partial read of %d bytes", actual);
        ASSERT(actual < kSectorSize && actual >= 0);
    }

    /*
     * We've read the data, but we can't use it.  We're a sector editor,
     * not a file editor, and we need to get the actual sector data without
     * EOF trimming or CP/M 0xe5 removal.
     */
    fpFile->GetLastLocationRead(&fTrack, &fSector);
    if (fTrack == A2File::kLastWasSparse && fSector == A2File::kLastWasSparse)

        ;
#endif

    fAlertMsg = "";

    dierr = fpOpenFile->GetStorage(fSectorIdx, &fTrack, &fSector);
    if (dierr == kDIErrInvalidIndex && fSectorIdx == 0) {
        // no first sector; should only happen on CP/M
        //FillWithPattern(fSectorData, sizeof(fSectorData), _T("EMPTY "));
        CheckedLoadString(&fAlertMsg, IDS_DISKEDITMSG_EMPTY);
    } else if (dierr != kDIErrNone) {
        CString msg, failed;
        CheckedLoadString(&failed, IDS_FAILED);
        msg.Format(IDS_DISKEDIT_FIRDFAILED, DiskImgLib::DIStrError(dierr));
        MessageBox(msg, failed, MB_OK);
        CheckedLoadString(&fAlertMsg, IDS_FAILED);
        // TO DO: mark contents as invalid, so editing fails
        return -1;
    } else {
        if (fTrack == 0 && fSector == 0) {
            LOGI("LoadData Sparse sector");
            //FillWithPattern(fSectorData, sizeof(fSectorData), _T("SPARSE "));
            fAlertMsg.Format(IDS_DISKEDITMSG_SPARSE, fSectorIdx);
        } else {
            LOGI("LoadData reading T=%d S=%d", fTrack, fSector);

            dierr = fpDiskFS->GetDiskImg()->ReadTrackSector(fTrack, fSector,
                        fSectorData);
            if (dierr != kDIErrNone) {
                //CString msg;
                //CString err;
                //err.LoadString(IDS_ERROR);
                //msg.Format(IDS_DISKEDIT_NOREADTS, fTrack, fSector);
                //MessageBox(msg, err, MB_OK|MB_ICONSTOP);
                CheckedLoadString(&fAlertMsg, IDS_DISKEDITMSG_BADSECTOR);
                //return -1;
            }
        }
    }

    SetSpinner(IDC_DISKEDIT_TRACKSPIN, fTrack);
    SetSpinner(IDC_DISKEDIT_SECTORSPIN, fSector);

    CWnd* pWnd;
    pWnd = GetDlgItem(IDC_DISKEDIT_PREV);
    ASSERT(pWnd != NULL);
    pWnd->EnableWindow(fSectorIdx > 0);
    if (!pWnd->IsWindowEnabled() && GetFocus() == NULL)
        GetDlgItem(IDC_DISKEDIT_NEXT)->SetFocus();

    pWnd = GetDlgItem(IDC_DISKEDIT_NEXT);
    ASSERT(pWnd != NULL);
    pWnd->EnableWindow(fSectorIdx+1 < fpOpenFile->GetSectorCount());
    if (!pWnd->IsWindowEnabled() && GetFocus() == NULL)
        GetDlgItem(IDC_DISKEDIT_PREV)->SetFocus();

    DisplayData();

    return 0;
}

void SectorFileEditDialog::OnReadPrev(void)
{
    if (fSectorIdx == 0)
        return;

    fSectorIdx--;
    ASSERT(fSectorIdx >= 0);
    LoadData();
}

void SectorFileEditDialog::OnReadNext(void)
{
    if (fSectorIdx+1 >= fpOpenFile->GetSectorCount())
        return;

    fSectorIdx++;
    ASSERT(fSectorIdx < fpOpenFile->GetSectorCount());
    LoadData();
}


/*
 * ===========================================================================
 *      BlockEditDialog
 * ===========================================================================
 */

/*
 * Rearrange the DiskEdit dialog (which defaults to SectorEdit mode) to
 * accommodate block editing.
 */
BOOL BlockEditDialog::OnInitDialog(void)
{
    /*
     * Get rid of the "sector" input item, and change the "track" input
     * item to accept blocks instead.
     */
    CWnd* pWnd;

    pWnd = GetDlgItem(IDC_STEXT_SECTOR);
    pWnd->DestroyWindow();
    pWnd = GetDlgItem(IDC_DISKEDIT_SECTOR);
    pWnd->DestroyWindow();
    pWnd = GetDlgItem(IDC_DISKEDIT_SECTORSPIN);
    pWnd->DestroyWindow();

    CString blockStr;
    //blockStr.LoadString(IDS_BLOCK);
    blockStr.Format(L"Block (%d):", fpDiskFS->GetDiskImg()->GetNumBlocks());
    pWnd = GetDlgItem(IDC_STEXT_TRACK);
    ASSERT(pWnd != NULL);
    pWnd->SetWindowText(blockStr);

    /*
     * Increase the size of the window to accommodate the larger block size.
     */
    const int kStretchHeight = 250;
    CRect rect;

    GetWindowRect(&rect);
    rect.bottom += kStretchHeight;
    MoveWindow(&rect);

    CRichEditCtrl* pEdit = (CRichEditCtrl*)GetDlgItem(IDC_DISKEDIT_EDIT);
    ASSERT(pEdit != NULL);
    CRect inner;
    pEdit->GetRect(&inner);
    inner.bottom += kStretchHeight;
    pEdit->GetWindowRect(&rect);
    ScreenToClient(&rect);
    rect.bottom += kStretchHeight;
    pEdit->MoveWindow(&rect);
    pEdit->SetRect(&inner);

    MoveControl(this, IDC_DISKEDIT_DONE, 0, kStretchHeight);
    MoveControl(this, IDC_DISKEDIT_OPENFILE, 0, kStretchHeight);
    MoveControl(this, IDC_DISKEDIT_SUBVOLUME, 0, kStretchHeight);
    MoveControl(this, IDHELP, 0, kStretchHeight);
    MoveControl(this, IDC_DISKEDIT_NIBBLE_PARMS, 0, kStretchHeight);

    /*
     * Do base-class construction.
     */
    DiskEditDialog::OnInitDialog();

    /*
     * Configure the spin button.  We use the "track" spin button for blocks.
     */
    MySpinCtrl* pSpin;
    pSpin = (MySpinCtrl*)GetDlgItem(IDC_DISKEDIT_TRACKSPIN);
    ASSERT(pSpin != NULL);
    pSpin->SetRange32(0, fpDiskFS->GetDiskImg()->GetNumBlocks()-1);
    pSpin->SetPos(0);

    /* give us something to look at */
    if (LoadData() != 0) {
        LOGI("WHOOPS: LoadData() failed, but we're in OnInitDialog");
    }

    return TRUE;
}

#if 0
/*
 * Move a control so it maintains its same position relative to the bottom
 * and right edges.
 */
void
BlockEditDialog::MoveControl(int id, int deltaX, int deltaY)
{
    CWnd* pWnd;
    CRect rect;

    pWnd = GetDlgItem(id);
    ASSERT(pWnd != NULL);

    pWnd->GetWindowRect(&rect);
    ScreenToClient(&rect);
    rect.left += deltaX;
    rect.right += deltaX;
    rect.top += deltaY;
    rect.bottom += deltaY;
    pWnd->MoveWindow(&rect, TRUE);
}
#endif


int BlockEditDialog::LoadData(void)
{
    //LOGI("BED LoadData");
    ASSERT(fpDiskFS != NULL);
    ASSERT(fpDiskFS->GetDiskImg() != NULL);

    if (ReadSpinner(IDC_DISKEDIT_TRACKSPIN, &fBlock) != 0)
        return -1;

    LOGI("LoadData reading block=%d", fBlock);

    fAlertMsg = "";
    DIError dierr;
    dierr = fpDiskFS->GetDiskImg()->ReadBlock(fBlock, fBlockData);
    if (dierr != kDIErrNone) {
        LOGI("BED block read failed: %hs", DiskImgLib::DIStrError(dierr));
        //CString msg;
        //CString err;
        //err.LoadString(IDS_ERROR);
        //msg.Format(IDS_DISKEDIT_NOREADBLOCK, fBlock);
        //MessageBox(msg, err, MB_OK|MB_ICONSTOP);
        CheckedLoadString(&fAlertMsg, IDS_DISKEDITMSG_BADBLOCK);
        //return -1;
    }

    DisplayData();

    return 0;
}

void BlockEditDialog::OnDoRead(void)
{
    LoadData();
}

void BlockEditDialog::OnDoWrite(void)
{
    MessageBox(L"Write!");
}

void BlockEditDialog::OnReadPrev(void)
{
    if (fBlock == 0)
        return;

    fBlock--;
    SetSpinner(IDC_DISKEDIT_TRACKSPIN, fBlock);
    LoadData();
}

void
BlockEditDialog::OnReadNext(void)
{
    ASSERT(fpDiskFS != NULL);
    if (fBlock == fpDiskFS->GetDiskImg()->GetNumBlocks() - 1)
        return;

    fBlock++;
    SetSpinner(IDC_DISKEDIT_TRACKSPIN, fBlock);
    LoadData();
}

void BlockEditDialog::OnOpenFile(void)
{
    DEFileDialog fileDialog(this);

    if (fileDialog.DoModal() == IDOK) {
        BlockFileEditDialog fileEdit(this, this);
        A2File* pFile;
        A2FileDescr* pOpenFile = NULL;
        DIError dierr;

        dierr = OpenFile(fileDialog.fName, fileDialog.fOpenRsrcFork != 0,
                    &pFile, &pOpenFile);
        if (dierr != kDIErrNone)
            return;

        fileEdit.SetupFile(fileDialog.fName, fileDialog.fOpenRsrcFork != 0,
            pFile, pOpenFile);
        fileEdit.SetPositionShift(8);
        (void) fileEdit.DoModal();

        pOpenFile->Close();
    }
}


/*
 * ===========================================================================
 *      BlockFileEditDialog
 * ===========================================================================
 */

BOOL BlockFileEditDialog::OnInitDialog(void)
{
    BOOL retval;

    /* do base class first */
    retval = BlockEditDialog::OnInitDialog();

    /* disable direct entry of tracks and Blocks */
    CWnd* pWnd;
    pWnd = GetDlgItem(IDC_DISKEDIT_TRACKSPIN);
    ASSERT(pWnd != NULL);
    pWnd->EnableWindow(FALSE);

    /* disallow opening of sub-volumes and files */
    pWnd = GetDlgItem(IDC_DISKEDIT_OPENFILE);
    pWnd->EnableWindow(FALSE);
    pWnd = GetDlgItem(IDC_DISKEDIT_SUBVOLUME);
    pWnd->EnableWindow(FALSE);

    CEdit* pEdit;
    pEdit = (CEdit*) GetDlgItem(IDC_DISKEDIT_TRACK);
    ASSERT(pEdit != NULL);
    pEdit->SetReadOnly(TRUE);

    /* set the window title */
    CString title;
    CString rsrcIndic;
    CheckedLoadString(&rsrcIndic, IDS_INDIC_RSRC);
    title.Format(L"Disk Viewer - %hs%ls (%I64d bytes)",
        (LPCSTR) fpFile->GetPathName(),  // use fpFile version to get case
        fOpenRsrcFork ? (LPCWSTR)rsrcIndic : L"", (LONGLONG) fLength);
    SetWindowText(title);

    return retval;
}

int BlockFileEditDialog::LoadData(void)
{
    ASSERT(fpDiskFS != NULL);
    ASSERT(fpDiskFS->GetDiskImg() != NULL);

    DIError dierr;
    LOGI("BFED LoadData reading index=%d", fBlockIdx);

#if 0
    LOGI("LoadData reading offset=%d", fOffset);
    size_t actual = 0;
    dierr = fpFile->Seek(fOffset, EmbeddedFD::kSeekSet);
    if (dierr == kDIErrNone) {
        dierr = fpFile->Read(fBlockData, 1 /*kBlockSize*/, &actual);
    }
    if (dierr != kDIErrNone) {
        CString msg, failed;
        failed.LoadString(IDS_FAILED);
        msg.Format(IDS_DISKEDIT_FIRDFAILED, DiskImg::DIStrError(dierr));
        MessageBox(msg, failed, MB_OK);
        // TO DO: mark contents as invalid, so editing fails
        return -1;
    }

    if (actual != kBlockSize) {
        LOGI(" BFED partial read of %d bytes", actual);
        ASSERT(actual < kBlockSize && actual >= 0);
    }

    /*
     * We've read the data, but we can't use it.  We're a Block editor,
     * not a file editor, and we need to get the actual Block data without
     * EOF trimming or CP/M 0xe5 removal.
     */
    fpFile->GetLastLocationRead(&fBlock);
    if (fBlock == A2File::kLastWasSparse)
        ;
#endif

    fAlertMsg = "";

    dierr = fpOpenFile->GetStorage(fBlockIdx, &fBlock);
    if (dierr == kDIErrInvalidIndex && fBlockIdx == 0) {
        // no first sector; should only happen on CP/M
        //FillWithPattern(fBlockData, sizeof(fBlockData), _T("EMPTY "));
        CheckedLoadString(&fAlertMsg, IDS_DISKEDITMSG_EMPTY);
    } else if (dierr != kDIErrNone) {
        CString msg, failed;
        CheckedLoadString(&failed, IDS_FAILED);
        msg.Format(IDS_DISKEDIT_FIRDFAILED, DiskImgLib::DIStrError(dierr));
        MessageBox(msg, failed, MB_OK);
        CheckedLoadString(&fAlertMsg, IDS_FAILED);
        // TO DO: mark contents as invalid, so editing fails
        return -1;
    } else {
        if (fBlock == 0) {
            LOGI("LoadData Sparse block");
            //FillWithPattern(fBlockData, sizeof(fBlockData), _T("SPARSE "));
            fAlertMsg.Format(IDS_DISKEDITMSG_SPARSE, fBlockIdx);
        } else {
            LOGI("LoadData reading B=%d", fBlock);

            dierr = fpDiskFS->GetDiskImg()->ReadBlock(fBlock, fBlockData);
            if (dierr != kDIErrNone) {
                //CString msg;
                //CString err;
                //err.LoadString(IDS_ERROR);
                //msg.Format(IDS_DISKEDIT_NOREADBLOCK, fBlock);
                //MessageBox(msg, err, MB_OK|MB_ICONSTOP);
                CheckedLoadString(&fAlertMsg, IDS_DISKEDITMSG_BADBLOCK);
                //return -1;
            }
        }
    }

    SetSpinner(IDC_DISKEDIT_TRACKSPIN, fBlock);

    CWnd* pWnd;
    pWnd = GetDlgItem(IDC_DISKEDIT_PREV);
    ASSERT(pWnd != NULL);
    pWnd->EnableWindow(fBlockIdx > 0);
    if (!pWnd->IsWindowEnabled() && GetFocus() == NULL)
        GetDlgItem(IDC_DISKEDIT_NEXT)->SetFocus();

    pWnd = GetDlgItem(IDC_DISKEDIT_NEXT);
    ASSERT(pWnd != NULL);
    pWnd->EnableWindow(fBlockIdx+1 < fpOpenFile->GetBlockCount());
    if (!pWnd->IsWindowEnabled() && GetFocus() == NULL)
        GetDlgItem(IDC_DISKEDIT_PREV)->SetFocus();

    DisplayData();

    return 0;
}

void BlockFileEditDialog::OnReadPrev(void)
{
    if (fBlockIdx == 0)
        return;

    fBlockIdx--;
    ASSERT(fBlockIdx >= 0);
    LoadData();
}

void BlockFileEditDialog::OnReadNext(void)
{
    if (fBlockIdx+1 >= fpOpenFile->GetBlockCount())
        return;

    fBlockIdx++;
    ASSERT(fBlockIdx < fpOpenFile->GetBlockCount());
    LoadData();
}


/*
 * ===========================================================================
 *      NibbleEditDialog
 * ===========================================================================
 */

BOOL NibbleEditDialog::OnInitDialog(void)
{
    /*
     * Get rid of the "sector" input item.
     */
    CWnd* pWnd;

    pWnd = GetDlgItem(IDC_STEXT_SECTOR);
    pWnd->DestroyWindow();
    pWnd = GetDlgItem(IDC_DISKEDIT_SECTOR);
    pWnd->DestroyWindow();
    pWnd = GetDlgItem(IDC_DISKEDIT_SECTORSPIN);
    pWnd->DestroyWindow();

    CString trackStr;
    trackStr.Format(L"Track (%d):", fpDiskFS->GetDiskImg()->GetNumTracks());
    pWnd = GetDlgItem(IDC_STEXT_TRACK);
    ASSERT(pWnd != NULL);
    pWnd->SetWindowText(trackStr);

    /*
     * Increase the size of the window so it's the same height as blocks.
     *
     * NOTE: using a pixel constant is probably bad.  We want to use something
     * like GetTextMetrics, but I'm not sure how to get that without a
     * device context.
     */
    CRichEditCtrl* pEdit = (CRichEditCtrl*)GetDlgItem(IDC_DISKEDIT_EDIT);
    ASSERT(pEdit != NULL);
    const int kStretchHeight = 249;
    CRect rect;

    GetWindowRect(&rect);
    rect.bottom += kStretchHeight;
    MoveWindow(&rect);

    /*
     * Must postpone resize of edit ctrl until after data has been loaded, or
     * scroll bars fail to appear under Win98.  Makes no sense whatsoever, but
     * that's Windows for you.
     */
#if 0
    CRect inner;
    pEdit->GetRect(&inner);
    inner.bottom += kStretchHeight;
    pEdit->GetWindowRect(&rect);
    ScreenToClient(&rect);
    rect.bottom += kStretchHeight;
    pEdit->MoveWindow(&rect);
    pEdit->SetRect(&inner);
#endif

    /* show the scroll bar */
    pEdit->ShowScrollBar(SB_VERT);

    MoveControl(this, IDC_DISKEDIT_DONE, 0, kStretchHeight);
    MoveControl(this, IDC_DISKEDIT_OPENFILE, 0, kStretchHeight);
    MoveControl(this, IDC_DISKEDIT_SUBVOLUME, 0, kStretchHeight);
    MoveControl(this, IDHELP, 0, kStretchHeight);
    MoveControl(this, IDC_DISKEDIT_NIBBLE_PARMS, 0, kStretchHeight);

    /* disable opening of files and sub-volumes */
    pWnd = GetDlgItem(IDC_DISKEDIT_OPENFILE);
    pWnd->EnableWindow(FALSE);
    pWnd = GetDlgItem(IDC_DISKEDIT_SUBVOLUME);
    pWnd->EnableWindow(FALSE);

    /*
     * Do base-class construction.
     */
    DiskEditDialog::OnInitDialog();

    /*
     * This currently has no effect on the nibble editor.  Someday we may
     * want to highlight and/or decode address fields.
     */
    pWnd = GetDlgItem(IDC_DISKEDIT_NIBBLE_PARMS);
    pWnd->EnableWindow(FALSE);

    /*
     * Configure the track spin button.
     */
    MySpinCtrl* pSpin;
    pSpin = (MySpinCtrl*)GetDlgItem(IDC_DISKEDIT_TRACKSPIN);
    ASSERT(pSpin != NULL);
    pSpin->SetRange32(0, fpDiskFS->GetDiskImg()->GetNumTracks()-1);
    pSpin->SetPos(0);

    /* give us something to look at */
    LoadData();

    return TRUE;
}

int NibbleEditDialog::LoadData(void)
{
    //LOGI("BED LoadData");
    ASSERT(fpDiskFS != NULL);
    ASSERT(fpDiskFS->GetDiskImg() != NULL);

    if (ReadSpinner(IDC_DISKEDIT_TRACKSPIN, &fTrack) != 0)
        return -1;

    LOGI("LoadData reading track=%d", fTrack);

    fAlertMsg = "";
    DIError dierr;
    dierr = fpDiskFS->GetDiskImg()->ReadNibbleTrack(fTrack, fNibbleData,
                &fNibbleDataLen);
    if (dierr != kDIErrNone) {
        LOGI("NED track read failed: %hs", DiskImgLib::DIStrError(dierr));
        CheckedLoadString(&fAlertMsg, IDS_DISKEDITMSG_BADTRACK);
    }

    DisplayData();

    return 0;
}

void NibbleEditDialog::OnDoRead(void)
{
    LoadData();
}

void NibbleEditDialog::OnDoWrite(void)
{
    MessageBox(L"Write!");
}

void NibbleEditDialog::OnReadPrev(void)
{
    if (fTrack == 0)
        return;

    fTrack--;
    SetSpinner(IDC_DISKEDIT_TRACKSPIN, fTrack);
    LoadData();
}

void NibbleEditDialog::OnReadNext(void)
{
    ASSERT(fpDiskFS != NULL);
    if (fTrack == fpDiskFS->GetDiskImg()->GetNumTracks() - 1)
        return;

    fTrack++;
    SetSpinner(IDC_DISKEDIT_TRACKSPIN, fTrack);
    LoadData();
}
