/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * File actions.  These are actually part of MainWindow, but for readability
 * these are split into their own file.
 */
#include "stdafx.h"
#include "Main.h"
#include "ViewFilesDialog.h"
//#include "ViewOptionsDialog.h"
#include "ChooseDirDialog.h"
#include "AddFilesDialog.h"
#include "CreateSubdirDialog.h"
#include "ExtractOptionsDialog.h"
#include "UseSelectionDialog.h"
#include "RecompressOptionsDialog.h"
#include "ConvDiskOptionsDialog.h"
#include "ConvFileOptionsDialog.h"
#include "EditCommentDialog.h"
#include "EditPropsDialog.h"
#include "RenameVolumeDialog.h"
#include "ConfirmOverwriteDialog.h"
#include "ImageFormatDialog.h"
#include "FileNameConv.h"
#include "GenericArchive.h"
#include "NufxArchive.h"
#include "DiskArchive.h"
#include "ChooseAddTargetDialog.h"
#include "CassetteDialog.h"
#include "BasicImport.h"
//#include "../util/UtilLib.h"
#include "../diskimg/TwoImg.h"
#include <errno.h>


/*
 * ==========================================================================
 *		View
 * ==========================================================================
 */

/*
 * View a file stored in the archive.
 *
 * Control bounces back through Get*FileText() to get the actual
 * data to view.
 */
void
MainWindow::OnActionsView(void)
{
	HandleView();
}
void
MainWindow::OnUpdateActionsView(CCmdUI* pCmdUI)
{
	pCmdUI->Enable(fpContentList != nil &&
		fpContentList->GetSelectedCount() > 0);
}


/*
 * Handle a request to view stuff.
 *
 * If "query" is set, we ask the user to confirm some choices.  If not, we
 * just go with the defaults.
 *
 * We include "damaged" files so that we can show the user a nice message
 * about how the file is damaged.
 */
void
MainWindow::HandleView(void)
{
	ASSERT(fpContentList != nil);

	SelectionSet selSet;
	int threadMask = GenericEntry::kAnyThread | GenericEntry::kAllowDamaged |
		GenericEntry::kAllowDirectory | GenericEntry::kAllowVolumeDir;
	selSet.CreateFromSelection(fpContentList, threadMask);
	selSet.Dump();

	if (selSet.GetNumEntries() == 0) {
		MessageBox("Nothing viewable found.",
			"No match", MB_OK | MB_ICONEXCLAMATION);
		return;
	}

	//fpSelSet = &selSet;

	ViewFilesDialog vfd(this);
	vfd.SetSelectionSet(&selSet);
	vfd.SetTextTypeFace(fPreferences.GetPrefString(kPrViewTextTypeFace));
	vfd.SetTextPointSize(fPreferences.GetPrefLong(kPrViewTextPointSize));
	vfd.SetNoWrapText(fPreferences.GetPrefBool(kPrNoWrapText));
	vfd.DoModal();

	//fpSelSet = nil;

	// remember which font they used (sticky pref, not in registry)
	fPreferences.SetPrefString(kPrViewTextTypeFace, vfd.GetTextTypeFace());
	fPreferences.SetPrefLong(kPrViewTextPointSize, vfd.GetTextPointSize());
	WMSG2("Preferences: saving view font %d-point '%s'\n",
		fPreferences.GetPrefLong(kPrViewTextPointSize),
		fPreferences.GetPrefString(kPrViewTextTypeFace));
}


/*
 * ==========================================================================
 *		Open as disk image
 * ==========================================================================
 */

/*
 * View a file stored in the archive.
 *
 * Control bounces back through Get*FileText() to get the actual
 * data to view.
 */
void
MainWindow::OnActionsOpenAsDisk(void)
{
	ASSERT(fpContentList != nil);
	ASSERT(fpContentList->GetSelectedCount() == 1);

	GenericEntry* pEntry = GetSelectedItem(fpContentList);
	if (pEntry->GetHasDiskImage())
		TmpExtractAndOpen(pEntry, GenericEntry::kDiskImageThread, kModeDiskImage);
	else
		TmpExtractAndOpen(pEntry, GenericEntry::kDataThread, kModeDiskImage);
}
void
MainWindow::OnUpdateActionsOpenAsDisk(CCmdUI* pCmdUI)
{
	const int kMinLen = 512 * 7;
	bool allow = false;

	if (fpContentList != nil && fpContentList->GetSelectedCount() == 1) {
		GenericEntry* pEntry = GetSelectedItem(fpContentList);
		if (pEntry != nil) {
			if ((pEntry->GetHasDataFork() || pEntry->GetHasDiskImage()) &&
				pEntry->GetUncompressedLen() > kMinLen)
			{
				allow = true;
			}
		}
	}
	pCmdUI->Enable(allow);
}


/*
 * ==========================================================================
 *		Add Files
 * ==========================================================================
 */

/*
 * Add files to an archive.
 */
void
MainWindow::OnActionsAddFiles(void)
{
	WMSG0("Add files!\n");
	AddFilesDialog addFiles(this);
	DiskImgLib::A2File* pTargetSubdir = nil;

	/*
	 * Special handling for adding files to disk images.
	 */
	if (fpOpenArchive->GetArchiveKind() == GenericArchive::kArchiveDiskImage)
	{
		if (!ChooseAddTarget(&pTargetSubdir, &addFiles.fpTargetDiskFS))
			return;
	}

	addFiles.fStoragePrefix = "";
	addFiles.fIncludeSubfolders =
		fPreferences.GetPrefBool(kPrAddIncludeSubFolders);
	addFiles.fStripFolderNames =
		fPreferences.GetPrefBool(kPrAddStripFolderNames);
	addFiles.fOverwriteExisting =
		fPreferences.GetPrefBool(kPrAddOverwriteExisting);
	addFiles.fTypePreservation =
		fPreferences.GetPrefLong(kPrAddTypePreservation);
	addFiles.fConvEOL =
		fPreferences.GetPrefLong(kPrAddConvEOL);

	/* if they can't convert EOL when adding files, disable the option */
	if (!fpOpenArchive->GetCapability(GenericArchive::kCapCanConvEOLOnAdd)) {
		addFiles.fConvEOL = AddFilesDialog::kConvEOLNone;
		addFiles.fConvEOLEnable = false;
	}

	/*
	 * Disable editing of the storage prefix field.  Force pathname
	 * stripping to be on for non-hierarchical filesystems (i.e. everything
	 * but ProDOS and HFS).
	 */
	if (addFiles.fpTargetDiskFS != nil) {
		DiskImg::FSFormat format;
		format = addFiles.fpTargetDiskFS->GetDiskImg()->GetFSFormat();

		if (pTargetSubdir != nil) {
			ASSERT(!pTargetSubdir->IsVolumeDirectory());
			addFiles.fStoragePrefix = pTargetSubdir->GetPathName();
		}

		addFiles.fStripFolderNamesEnable = false;
		addFiles.fStoragePrefixEnable = false;
		switch (format) {
		case DiskImg::kFormatProDOS:
		case DiskImg::kFormatMacHFS:
			addFiles.fStripFolderNamesEnable = true;
			break;
		default:
			break;
		}
	}
	if (!addFiles.fStripFolderNamesEnable) {
		/* if we disabled it, we did so because it's mandatory */
		addFiles.fStripFolderNames = true;
	}

	addFiles.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrAddFileFolder);

	addFiles.DoModal();
	if (addFiles.GetExitStatus() == IDOK) {
		fPreferences.SetPrefBool(kPrAddIncludeSubFolders,
			addFiles.fIncludeSubfolders != 0);
		if (addFiles.fStripFolderNamesEnable) {
			// only update the pref if they had the ability to change it
			fPreferences.SetPrefBool(kPrAddStripFolderNames,
				addFiles.fStripFolderNames != 0);
		}
		fPreferences.SetPrefBool(kPrAddOverwriteExisting,
			addFiles.fOverwriteExisting != 0);
		fPreferences.SetPrefLong(kPrAddTypePreservation,
			addFiles.fTypePreservation);
		if (addFiles.fConvEOLEnable)
			fPreferences.SetPrefLong(kPrAddConvEOL,
				addFiles.fConvEOL);

		CString saveFolder = addFiles.GetFileNames();
		saveFolder = saveFolder.Left(addFiles.GetFileNameOffset());
		fPreferences.SetPrefString(kPrAddFileFolder, saveFolder);

		/*
		 * Set up a progress dialog and kick things off.
		 */
		bool result;
		fpActionProgress = new ActionProgressDialog;
		fpActionProgress->Create(ActionProgressDialog::kActionAdd, this);

		//fpContentList->Invalidate();		// don't allow redraws until done
		result = fpOpenArchive->BulkAdd(fpActionProgress, &addFiles);
		fpContentList->Reload();

		fpActionProgress->Cleanup(this);
		fpActionProgress = nil;
		if (result)
			SuccessBeep();
	} else {
		WMSG0("SFD bailed with Cancel\n");
	}
}
void
MainWindow::OnUpdateActionsAddFiles(CCmdUI* pCmdUI)
{
	pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly());
}


/*
 * Figure out where they want to add files.
 *
 * If the volume directory of a disk is chosen, "*ppTargetSubdir" will
 * be set to nil.
 */
bool
MainWindow::ChooseAddTarget(DiskImgLib::A2File** ppTargetSubdir,
	DiskImgLib::DiskFS** ppTargetDiskFS)
{
	ASSERT(ppTargetSubdir != nil);
	ASSERT(ppTargetDiskFS != nil);

	*ppTargetSubdir = nil;
	*ppTargetDiskFS = nil;

	GenericEntry* pEntry = GetSelectedItem(fpContentList);
	if (pEntry != nil &&
		(pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory ||
		 pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir))
	{
		/*
		 * They've selected a single subdirectory.  Add the files there.
		 */
		DiskEntry* pDiskEntry = (DiskEntry*) pEntry;
		*ppTargetSubdir = pDiskEntry->GetA2File();
		*ppTargetDiskFS = (*ppTargetSubdir)->GetDiskFS();
	} else {
		/*
		 * Nothing selected, non-subdir selected, or multiple files
		 * selected.  Whatever the case, pop up the choose target dialog.
		 *
		 * This works on DOS 3.3 and Pascal disks, because the absence
		 * of subdirectories means there's only one possible place to
		 * put the files.  We could short-circuit this code for anything
		 * but ProDOS and HFS, but we have to be careful about embedded
		 * sub-volumes.
		 */
		DiskArchive* pDiskArchive = (DiskArchive*) fpOpenArchive;

		WMSG0("Trying ChooseAddTarget\n");

		ChooseAddTargetDialog targetDialog(this);
		targetDialog.fpDiskFS = pDiskArchive->GetDiskFS();
		if (targetDialog.DoModal() != IDOK)
			return false;

		*ppTargetSubdir = targetDialog.fpChosenSubdir;
		*ppTargetDiskFS = targetDialog.fpChosenDiskFS;

		/* make sure the subdir is part of the diskfs */
		ASSERT(*ppTargetSubdir == nil ||
			(*ppTargetSubdir)->GetDiskFS() == *ppTargetDiskFS);
	}

	if (*ppTargetSubdir != nil && (*ppTargetSubdir)->IsVolumeDirectory())
		*ppTargetSubdir = nil;

	return true;
}


/*
 * ==========================================================================
 *		Add Disks
 * ==========================================================================
 */

/*
 * Add a disk to an archive.  Not all archive formats support disk images.
 *
 * We open a single disk archive file as a DiskImg, get the format
 * figured out, then write it block-by-block into a file chosen by the user.
 * Standard open/save dialogs work fine here.
 */
void
MainWindow::OnActionsAddDisks(void)
{
	DIError dierr;
	DiskImg img;
	CString failed, errMsg;
	CString openFilters, saveFolder;
	AddFilesDialog addOpts;

	WMSG0("Add disks!\n");

	failed.LoadString(IDS_FAILED);

	openFilters = kOpenDiskImage;
	openFilters += kOpenAll;
	openFilters += kOpenEnd;
	CFileDialog dlg(TRUE, "dsk", NULL, OFN_FILEMUSTEXIST, openFilters, this);
	dlg.m_ofn.lpstrTitle = "Add Disk Image";

	/* file is always opened read-only */
	dlg.m_ofn.Flags |= OFN_HIDEREADONLY;
	dlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrAddFileFolder);

	if (dlg.DoModal() != IDOK)
		goto bail;

	saveFolder = dlg.m_ofn.lpstrFile;
	saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset);
	fPreferences.SetPrefString(kPrAddFileFolder, saveFolder);

	/* open the image file and analyze it */
	dierr = img.OpenImage(dlg.GetPathName(), PathProposal::kLocalFssep, true);
	if (dierr != kDIErrNone) {
		errMsg.Format("Unable to open disk image: %s.",
			DiskImgLib::DIStrError(dierr));
		MessageBox(errMsg, failed, MB_OK|MB_ICONSTOP);
		goto bail;
	}

	if (img.AnalyzeImage() != kDIErrNone) {
		errMsg.Format("The file '%s' doesn't seem to hold a valid disk image.",
			dlg.GetPathName());
		MessageBox(errMsg, failed, MB_OK|MB_ICONSTOP);
		goto bail;
	}


	/* if requested (or necessary), verify the format */
	if (/*img.GetFSFormat() == DiskImg::kFormatUnknown ||*/
		img.GetSectorOrder() == DiskImg::kSectorOrderUnknown ||
		fPreferences.GetPrefBool(kPrQueryImageFormat))
	{
		ImageFormatDialog imf;

		imf.InitializeValues(&img);
		imf.fFileSource = dlg.GetPathName();
		imf.SetQueryDisplayFormat(false);

		WMSG2(" On entry, sectord=%d format=%d\n",
			imf.fSectorOrder, imf.fFSFormat);
		if (imf.fFSFormat == DiskImg::kFormatUnknown)
			imf.fFSFormat = DiskImg::kFormatGenericProDOSOrd;

		if (imf.DoModal() != IDOK) {
			WMSG0("User bailed on IMF dialog\n");
			goto bail;
		}

		WMSG2(" On exit, sectord=%d format=%d\n",
			imf.fSectorOrder, imf.fFSFormat);

		if (imf.fSectorOrder != img.GetSectorOrder() ||
			imf.fFSFormat != img.GetFSFormat())
		{
			WMSG0("Initial values overridden, forcing img format\n");
			dierr = img.OverrideFormat(img.GetPhysicalFormat(), imf.fFSFormat,
						imf.fSectorOrder);
			if (dierr != kDIErrNone) {
				errMsg.Format("Unable to access disk image using selected"
						   " parameters.  Error: %s.",
					DiskImgLib::DIStrError(dierr));
				MessageBox(errMsg, failed, MB_OK | MB_ICONSTOP);
				goto bail;
			}
		}
	}

	/*
	 * We want to read from the image the way that a ProDOS application
	 * would, which means forcing the FSFormat to generic ProDOS ordering.
	 * This way, ProDOS disks load serially, and DOS 3.3 disks load with
	 * their sectors swapped around.
	 */
	dierr = img.OverrideFormat(img.GetPhysicalFormat(),
				DiskImg::kFormatGenericProDOSOrd, img.GetSectorOrder());
	if (dierr != kDIErrNone) {
		errMsg.Format("Internal error: couldn't switch to generic ProDOS: %s.",
				DiskImgLib::DIStrError(dierr));
		MessageBox(errMsg, failed, MB_OK | MB_ICONSTOP);
		goto bail;
	}

	/*
	 * Set up an AddFilesDialog, but don't actually use it as a dialog.
	 * Instead, we just configure the various options appropriately.
	 *
	 * To conform to multi-file-selection semantics, we need to drop a
	 * null byte in right after the pathname.
	 */
	ASSERT(dlg.m_ofn.nFileOffset > 0);
	int len;
	len = strlen(dlg.m_ofn.lpstrFile) + 2;
	dlg.m_ofn.lpstrFile[dlg.m_ofn.nFileOffset-1] = '\0';
	addOpts.SetFileNames(dlg.m_ofn.lpstrFile, len, dlg.m_ofn.nFileOffset);
	addOpts.fStoragePrefix = "";
	addOpts.fIncludeSubfolders = false;
	addOpts.fStripFolderNames = false;
	addOpts.fOverwriteExisting = false;
	addOpts.fTypePreservation = AddFilesDialog::kPreserveTypes;
	addOpts.fConvEOL = AddFilesDialog::kConvEOLNone;
	addOpts.fConvEOLEnable = false;		// no EOL conversion on disk images!
	addOpts.fpDiskImg = &img;

	bool result;

	fpActionProgress = new ActionProgressDialog;
	fpActionProgress->Create(ActionProgressDialog::kActionAddDisk, this);

	//fpContentList->Invalidate();		// don't allow updates until done
	result = fpOpenArchive->AddDisk(fpActionProgress, &addOpts);
	fpContentList->Reload();

	fpActionProgress->Cleanup(this);
	fpActionProgress = nil;

	if (result)
		SuccessBeep();

bail:
	return;
}
void
MainWindow::OnUpdateActionsAddDisks(CCmdUI* pCmdUI)
{
	pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() &&
		fpOpenArchive->GetCapability(GenericArchive::kCapCanAddDisk));
}


/*
 * ==========================================================================
 *		Create Subdirectory
 * ==========================================================================
 */

/*
 * Create a subdirectory inside another subdirectory (or volume directory).
 *
 * Simply asserting that an existing subdir be selected in the list does
 * away with all sorts of testing.  Creating subdirs on DOS disks and NuFX
 * archives is impossible because neither has subdirs.  Nested volumes are
 * selected for us by the user.
 */
void
MainWindow::OnActionsCreateSubdir(void)
{
	CreateSubdirDialog csDialog;

	ASSERT(fpContentList != nil);
	ASSERT(fpOpenArchive != nil);
	ASSERT(!fpOpenArchive->IsReadOnly());

	GenericEntry* pEntry = GetSelectedItem(fpContentList);
	if (pEntry == nil) {
		// can happen for no selection or multi-selection; should not be here
		ASSERT(false);
		return;
	}
	if (pEntry->GetRecordKind() != GenericEntry::kRecordKindDirectory &&
		pEntry->GetRecordKind() != GenericEntry::kRecordKindVolumeDir)
	{
		CString errMsg;
		errMsg = "Please select a subdirectory.";
		ShowFailureMsg(this, errMsg, IDS_MB_APP_NAME);
		return;
	}

	WMSG1("Creating subdir in '%s'\n", pEntry->GetPathName());

	csDialog.fBasePath = pEntry->GetPathName();
	csDialog.fpArchive = fpOpenArchive;
	csDialog.fpParentEntry = pEntry;
	csDialog.fNewName = "New.Subdir";
	if (csDialog.DoModal() != IDOK)
		return;

	WMSG1("Creating '%s'\n", csDialog.fNewName);

	fpOpenArchive->CreateSubdir(this, pEntry, csDialog.fNewName);
	fpContentList->Reload();
}
void
MainWindow::OnUpdateActionsCreateSubdir(CCmdUI* pCmdUI)
{
	bool enable = fpContentList != nil && !fpOpenArchive->IsReadOnly() &&
		fpContentList->GetSelectedCount() == 1 &&
		fpOpenArchive->GetCapability(GenericArchive::kCapCanCreateSubdir);

	if (enable) {
		/* second-level check: make sure it's a subdir */
		GenericEntry* pEntry = GetSelectedItem(fpContentList);
		if (pEntry == nil) {
			ASSERT(false);
			return;
		}
		if (pEntry->GetRecordKind() != GenericEntry::kRecordKindDirectory &&
			pEntry->GetRecordKind() != GenericEntry::kRecordKindVolumeDir)
		{
			enable = false;
		}
	}

	pCmdUI->Enable(enable);
}


/*
 * ==========================================================================
 *		Extract
 * ==========================================================================
 */

/*
 * Extract files.
 */
void
MainWindow::OnActionsExtract(void)
{
	ASSERT(fpContentList != nil);

	/*
	 * Ask the user about various options.
	 */
	ExtractOptionsDialog extOpts(fpContentList->GetSelectedCount(), this);
	extOpts.fExtractPath = fPreferences.GetPrefString(kPrExtractFileFolder);
	extOpts.fConvEOL = fPreferences.GetPrefLong(kPrExtractConvEOL);
	extOpts.fConvHighASCII = fPreferences.GetPrefBool(kPrExtractConvHighASCII);
	extOpts.fIncludeDataForks = fPreferences.GetPrefBool(kPrExtractIncludeData);
	extOpts.fIncludeRsrcForks = fPreferences.GetPrefBool(kPrExtractIncludeRsrc);
	extOpts.fIncludeDiskImages = fPreferences.GetPrefBool(kPrExtractIncludeDisk);
	extOpts.fEnableReformat = fPreferences.GetPrefBool(kPrExtractEnableReformat);
	extOpts.fDiskTo2MG = fPreferences.GetPrefBool(kPrExtractDiskTo2MG);
	extOpts.fAddTypePreservation = fPreferences.GetPrefBool(kPrExtractAddTypePreservation);
	extOpts.fAddExtension = fPreferences.GetPrefBool(kPrExtractAddExtension);
	extOpts.fStripFolderNames = fPreferences.GetPrefBool(kPrExtractStripFolderNames);
	extOpts.fOverwriteExisting = fPreferences.GetPrefBool(kPrExtractOverwriteExisting);

	if (fpContentList->GetSelectedCount() > 0)
		extOpts.fFilesToExtract = ExtractOptionsDialog::kExtractSelection;
	else
		extOpts.fFilesToExtract = ExtractOptionsDialog::kExtractAll;

	if (extOpts.DoModal() != IDOK)
		return;

	if (extOpts.fExtractPath.Right(1) != "\\")
		extOpts.fExtractPath += "\\";

	/* push preferences back out */
	fPreferences.SetPrefString(kPrExtractFileFolder, extOpts.fExtractPath);
	fPreferences.SetPrefLong(kPrExtractConvEOL, extOpts.fConvEOL);
	fPreferences.SetPrefBool(kPrExtractConvHighASCII, extOpts.fConvHighASCII != 0);
	fPreferences.SetPrefBool(kPrExtractIncludeData, extOpts.fIncludeDataForks != 0);
	fPreferences.SetPrefBool(kPrExtractIncludeRsrc, extOpts.fIncludeRsrcForks != 0);
	fPreferences.SetPrefBool(kPrExtractIncludeDisk, extOpts.fIncludeDiskImages != 0);
	fPreferences.SetPrefBool(kPrExtractEnableReformat, extOpts.fEnableReformat != 0);
	fPreferences.SetPrefBool(kPrExtractDiskTo2MG, extOpts.fDiskTo2MG != 0);
	fPreferences.SetPrefBool(kPrExtractAddTypePreservation, extOpts.fAddTypePreservation != 0);
	fPreferences.SetPrefBool(kPrExtractAddExtension, extOpts.fAddExtension != 0);
	fPreferences.SetPrefBool(kPrExtractStripFolderNames, extOpts.fStripFolderNames != 0);
	fPreferences.SetPrefBool(kPrExtractOverwriteExisting, extOpts.fOverwriteExisting != 0);

	WMSG1("Requested extract path is '%s'\n", extOpts.fExtractPath);

	/*
	 * Create a "selection set" of things to display.
	 */
	SelectionSet selSet;
	int threadMask = 0;
	if (extOpts.fIncludeDataForks)
		threadMask |= GenericEntry::kDataThread;
	if (extOpts.fIncludeRsrcForks)
		threadMask |= GenericEntry::kRsrcThread;
	if (extOpts.fIncludeDiskImages)
		threadMask |= GenericEntry::kDiskImageThread;

	if (extOpts.fFilesToExtract == ExtractOptionsDialog::kExtractSelection) {
		selSet.CreateFromSelection(fpContentList, threadMask);
	} else {
		selSet.CreateFromAll(fpContentList, threadMask);
	}
	//selSet.Dump();

	if (selSet.GetNumEntries() == 0) {
		MessageBox("No files matched the selection criteria.",
			"No match", MB_OK | MB_ICONEXCLAMATION);
		return;
	}

	/*
	 * Set up the progress dialog then do the extraction.
	 */
	fpActionProgress = new ActionProgressDialog;
	fpActionProgress->Create(ActionProgressDialog::kActionExtract, this);
	DoBulkExtract(&selSet, &extOpts);
	fpActionProgress->Cleanup(this);
	fpActionProgress = nil;
}
void
MainWindow::OnUpdateActionsExtract(CCmdUI* pCmdUI)
{
	pCmdUI->Enable(fpContentList != nil);
}

/*
 * Handle a bulk extraction.
 *
 * IMPORTANT: since the pActionProgress dialog has the foreground, it's
 * vital that any MessageBox calls go through that.  Otherwise the
 * progress dialog message handler won't get disabled by MessageBox and
 * we can end up permanently hiding the dialog.  (Could also use
 * ::MessageBox or ::AfxMessageBox instead.)
 */
void
MainWindow::DoBulkExtract(SelectionSet* pSelSet,
	const ExtractOptionsDialog* pExtOpts)
{
	ReformatHolder* pHolder = nil;
	bool overwriteExisting, ovwrForAll;

	ASSERT(pSelSet != nil);
	ASSERT(fpActionProgress != nil);

	pSelSet->IterReset();

	/* set up our "overwrite existing files" logic */
	overwriteExisting = ovwrForAll = (pExtOpts->fOverwriteExisting != FALSE);

	while (true) {
		SelectionEntry* pSelEntry;
		bool result;

		/* make sure we service events */
		PeekAndPump();

		pSelEntry = pSelSet->IterNext();
		if (pSelEntry == nil) {
			SuccessBeep();
			break;		// out of while (all done!)
		}

		GenericEntry* pEntry = pSelEntry->GetEntry();
		if (pEntry->GetDamaged()) {
			WMSG1("Skipping '%s' due to damage\n", pEntry->GetPathName());
			continue;
		}

		/*
		 * Extract all parts of the file -- including those we don't actually
		 * intend to extract to disk -- and hold them in the ReformatHolder.
		 * Some formats, e.g. GWP, need the resource fork to do formatting,
		 * so it's important to have the resource fork available even if
		 * we're just going to throw it away.
		 *
		 * The selection set should have screened out anything totally
		 * inappropriate, e.g. files with nothing but a resource fork don't
		 * make it into the set if we're not extracting resource forks.
		 *
		 * We only want to reformat files, not disk images, directories,
		 * volume dirs, etc.  We have a reformatter for ProDOS directories,
		 * but (a) we don't explicitly extract subdirs, and (b) we'd really
		 * like directories to be directories so we can extract files into
		 * them.
		 */
		if (pExtOpts->ShouldTryReformat() &&
			(pEntry->GetRecordKind() == GenericEntry::kRecordKindFile ||
			 pEntry->GetRecordKind() == GenericEntry::kRecordKindForkedFile))
		{
			fpActionProgress->SetArcName(pEntry->GetDisplayName());
			fpActionProgress->SetFileName(_T("-"));
			SET_PROGRESS_BEGIN();

			if (GetFileParts(pEntry, &pHolder) == 0) {
				/*
				 * Use the prefs, but disable generic text conversion, so
				 * that we default to "raw".  That way we will use the text
				 * conversion that the user has specified in the "extract"
				 * dialog.
				 *
				 * We might want to just disable any "always"-level
				 * reformatter, but that would require tweaking the reformat
				 * code to return "raw" when nothing applies.
				 */
				ConfigureReformatFromPreferences(pHolder);
				pHolder->SetReformatAllowed(ReformatHolder::kReformatTextEOL_HA,
					false);

				pHolder->SetSourceAttributes(
					pEntry->GetFileType(),
					pEntry->GetAuxType(),
					ReformatterSourceFormat(pEntry->GetSourceFS()),
					pEntry->GetFileNameExtension());
				pHolder->TestApplicability();
			}
		}

		if (pExtOpts->fIncludeDataForks && pEntry->GetHasDataFork()) {
			result = ExtractEntry(pEntry, GenericEntry::kDataThread,
						pHolder, pExtOpts, &overwriteExisting, &ovwrForAll);
			if (!result)
				break;
		}
		if (pExtOpts->fIncludeRsrcForks && pEntry->GetHasRsrcFork()) {
			result = ExtractEntry(pEntry, GenericEntry::kRsrcThread,
						pHolder, pExtOpts, &overwriteExisting, &ovwrForAll);
			if (!result)
				break;
		}
		if (pExtOpts->fIncludeDiskImages && pEntry->GetHasDiskImage()) {
			result = ExtractEntry(pEntry, GenericEntry::kDiskImageThread,
						pHolder, pExtOpts, &overwriteExisting, &ovwrForAll);
			if (!result)
				break;
		}
		delete pHolder;
		pHolder = nil;
	}

	// if they cancelled, delete the "stray"
	delete pHolder;
}

/*
 * Extract a single entry.
 *
 * If "pHolder" is non-nil, it holds the data from the file, and can be
 * used for formatted or non-formatted output.  If it's nil, we need to
 * extract the data ourselves.
 *
 * Returns "true" on success, "false" on failure.
 */
bool
MainWindow::ExtractEntry(GenericEntry* pEntry, int thread,
	ReformatHolder* pHolder, const ExtractOptionsDialog* pExtOpts,
	bool* pOverwriteExisting, bool* pOvwrForAll)
{
	/*
	 * This first bit of setup is the same for every file.  However, it's
	 * pretty quick, and it's easier to pass "pExtOpts" in than all of
	 * this stuff, so we just do it every time.
	 */
	GenericEntry::ConvertEOL convEOL;
	GenericEntry::ConvertHighASCII convHA;
	bool convTextByType = false;


	/* translate the EOL conversion mode into GenericEntry terms */
	switch (pExtOpts->fConvEOL) {
	case ExtractOptionsDialog::kConvEOLNone:
		convEOL = GenericEntry::kConvertEOLOff;
		break;
	case ExtractOptionsDialog::kConvEOLType:
		convEOL = GenericEntry::kConvertEOLOff;
		convTextByType = true;
		break;
	case ExtractOptionsDialog::kConvEOLAuto:
		convEOL = GenericEntry::kConvertEOLAuto;
		break;
	case ExtractOptionsDialog::kConvEOLAll:
		convEOL = GenericEntry::kConvertEOLOn;
		break;
	default:
		ASSERT(false);
		convEOL = GenericEntry::kConvertEOLOff;
		break;
	}
	if (pExtOpts->fConvHighASCII)
		convHA = GenericEntry::kConvertHAAuto;
	else
		convHA = GenericEntry::kConvertHAOff;

	//WMSG2("  DBE initial text conversion: eol=%d ha=%d\n",
	//	convEOL, convHA);


	ReformatHolder holder;
	CString outputPath;
	CString failed, errMsg;
	failed.LoadString(IDS_FAILED);
	bool writeFailed = false;
	bool extractAs2MG = false;
	char* reformatText = nil;
	MyDIBitmap* reformatDib = nil;

	ASSERT(pEntry != nil);

	/*
	 * If we're interested in extracting disk images as 2MG files,
	 * see if we want to handle this one that way.
	 *
	 * If they said "don't extract disk images", the images should
	 * have been culled from the selection set earlier.
	 */
	if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDisk) {
		ASSERT(pExtOpts->fIncludeDiskImages);
		if (pExtOpts->fDiskTo2MG) {
			if (pEntry->GetUncompressedLen() > 0 &&
				(pEntry->GetUncompressedLen() % TwoImgHeader::kBlockSize) == 0)
			{
				extractAs2MG = true;
			} else {
				WMSG2("Not extracting funky image '%s' as 2MG (len=%ld)\n",
					pEntry->GetPathName(), pEntry->GetUncompressedLen());
			}
		}
	}

	/*
	 * Convert the archived pathname to something suitable for the
	 * local machine (i.e. Win32).
	 */
	PathProposal pathProp;
	CString convName, convNameExtd, convNameExtdPlus;
	pathProp.Init(pEntry);
	pathProp.fThreadKind = thread;
	if (pExtOpts->fStripFolderNames)
		pathProp.fJunkPaths = true;

	pathProp.ArchiveToLocal();
	convName = pathProp.fLocalPathName;

	/* run it through again, this time with optional type preservation */
	if (pExtOpts->fAddTypePreservation)
		pathProp.fPreservation = true;
	pathProp.ArchiveToLocal();
	convNameExtd = pathProp.fLocalPathName;

	/* and a 3rd time, also taking additional extensions into account */
	if (pExtOpts->fAddExtension)
		pathProp.fAddExtension = true;
	pathProp.ArchiveToLocal();
	convNameExtdPlus = pathProp.fLocalPathName;

	/*
	 * Prepend the extraction dir to the local pathname.  We also add
	 * the sub-volume name (if any), which should already be a valid
	 * Win32 directory name.  We can't add it earlier because the fssep
	 * char might be '\0'.
	 */
	ASSERT(pExtOpts->fExtractPath.Right(1) == "\\");
	CString adjustedExtractPath(pExtOpts->fExtractPath);
	if (!pExtOpts->fStripFolderNames && pEntry->GetSubVolName() != nil) {
		adjustedExtractPath += pEntry->GetSubVolName();
		adjustedExtractPath += "\\";
	}
	outputPath = adjustedExtractPath;
	outputPath += convNameExtdPlus;


	ReformatOutput* pOutput = nil;

	/*
	 * If requested, try to reformat this file.
	 */
	if (pHolder != nil) {
		ReformatHolder::ReformatPart part = ReformatHolder::kPartUnknown;
		ReformatHolder::ReformatID id;
		CString title;
		//int result;

		switch (thread) {
		case GenericEntry::kDataThread:
			part = ReformatHolder::kPartData;
			break;
		case GenericEntry::kRsrcThread:
			part = ReformatHolder::kPartRsrc;
			break;
		case GenericEntry::kDiskImageThread:
			part = ReformatHolder::kPartData;
			break;
		case GenericEntry::kCommentThread:
		default:
			assert(false);
			return false;
		}

		fpActionProgress->SetFileName(_T("(reformatting)"));
		id = pHolder->FindBest(part);

		{
			CWaitCursor waitc;
			pOutput = pHolder->Apply(part, id);
		}

		if (pOutput != nil) {
			/* use output pathname without preservation */
			CString tmpPath;
			bool goodReformat = true;
			bool noChangePath = false;

			tmpPath = adjustedExtractPath;
			tmpPath += convName;

			CString lastFour = tmpPath.Right(4);

			/*
			 * Tack on a file extension identifying the reformatted
			 * contents.  If the filename already has the correct
			 * extension, don't tack it on again.
			 */
			switch (pOutput->GetOutputKind()) {
			case ReformatOutput::kOutputText:
				if (lastFour.CompareNoCase(".txt") != 0)
					tmpPath += ".txt";
				break;
			case ReformatOutput::kOutputRTF:
				if (lastFour.CompareNoCase(".rtf") != 0)
					tmpPath += ".rtf";
				break;
			case ReformatOutput::kOutputCSV:
				if (lastFour.CompareNoCase(".csv") != 0)
					tmpPath += ".csv";
				break;
			case ReformatOutput::kOutputBitmap:
				if (lastFour.CompareNoCase(".bmp") != 0)
					tmpPath += ".bmp";
				break;
			case ReformatOutput::kOutputRaw:
				noChangePath = true;
				break;
			default:
				// kOutputErrorMsg, kOutputUnknown
				goodReformat = false;
				break;
			}

			if (goodReformat) {
				if (!noChangePath)
					outputPath = tmpPath;
			} else {
				delete pOutput;
				pOutput = nil;
			}
		}
	}
	if (extractAs2MG) {
		/*
		 * Reduce to base name and add 2IMG suffix.  Would be nice to keep
		 * the non-extended file type preservation stuff, but right now we
		 * only expect unadorned sectors for that (and so does NuLib2).
		 */
		outputPath = adjustedExtractPath;
		outputPath += convName;
		outputPath += ".2mg";
	}

	/* update the display in case we renamed it */
	if (outputPath != fpActionProgress->GetFileName()) {
		WMSG2(" Renamed our output, from '%s' to '%s'\n",
			(LPCTSTR) fpActionProgress->GetFileName(), outputPath);
		fpActionProgress->SetFileName(outputPath);
	}

	/*
	 * Update the progress meter output filename, and reset the thermometer.
	 */
	fpActionProgress->SetArcName(pathProp.fStoredPathName);
	fpActionProgress->SetFileName(outputPath);
	WMSG2("Extracting from '%s' to '%s'\n",
		pathProp.fStoredPathName, outputPath);
	SET_PROGRESS_BEGIN();

	/*
	 * Open the output file.
	 *
	 * Returns IDCANCEL on failures as well as user cancellation.
	 */
	FILE* fp = nil;
	int result;
	result = OpenOutputFile(&outputPath, pathProp, pEntry->GetModWhen(),
				pOverwriteExisting, pOvwrForAll, &fp);
	if (result == IDCANCEL) {
		// no messagebox for this one
		delete pOutput;
		return false;
	}


	/* update the display in case they renamed the file */
	if (outputPath != fpActionProgress->GetFileName()) {
		WMSG2(" Detected rename, from '%s' to '%s'\n",
			(LPCTSTR) fpActionProgress->GetFileName(), outputPath);
		fpActionProgress->SetFileName(outputPath);
	}

	if (fp == nil) {
		/* looks like they elected to skip extraction of this file */
		delete pOutput;
		return true;
	}

	//EventPause(500);	// DEBUG DEBUG


	/*
	 * Handle "extract as 2MG" by writing a 2MG header to the start
	 * of the file before we hand off to the extraction function.
	 *
	 * NOTE: we're currently assuming that we're extracting an image
	 * in ProDOS sector order.  This is a valid assumption so long as
	 * we're only pulling disk images out of ShrinkIt archives.
	 *
	 * We don't currently use the WriteFooter call here, because we're
	 * not adding comments.
	 */
	if (extractAs2MG) {
		TwoImgHeader header;
		header.InitHeader(TwoImgHeader::kImageFormatProDOS,
			(long) pEntry->GetUncompressedLen(),
			(long) (pEntry->GetUncompressedLen() / TwoImgHeader::kBlockSize));
		int err;
		ASSERT(ftell(fp) == 0);
		err = header.WriteHeader(fp);
		if (err != 0) {
			errMsg.Format("Unable to save 2MG file '%s': %s\n",
				outputPath, strerror(err));
			fpActionProgress->MessageBox(errMsg, failed,
				MB_OK | MB_ICONERROR);
			goto open_file_fail;
		}
		ASSERT(ftell(fp) == 64);	// size of 2MG header
	}


	/*
	 * In some cases we want to override the automatic text detection.
	 *
	 * If we're in "auto" mode, force conversion on for DOS/RDOS text files.
	 * This is important when "convHA" is off, because in high-ASCII mode
	 * we might not recognize text files for what they are.  We also
	 * consider 0x00 to be binary, which screws up random-access text.
	 *
	 * We don't want to do text conversion on disk images or resource
	 * forks, ever.  Turn them off here.
	 */
	GenericEntry::ConvertEOL thisConv;
	thisConv = convEOL;
	if (thisConv == GenericEntry::kConvertEOLAuto) {
		if (DiskImg::UsesDOSFileStructure(pEntry->GetSourceFS()) &&
			pEntry->GetFileType() == kFileTypeTXT)
		{
			WMSG0("Switching EOLAuto to EOLOn for DOS text file\n");
			thisConv = GenericEntry::kConvertEOLOn;
		}
	} else if (convTextByType) {
		/* force it on or off when in conv-by-type mode */
		if (pEntry->GetFileType() == kFileTypeTXT ||
			pEntry->GetFileType() == kFileTypeSRC)
		{
			WMSG0("Enabling EOL conv for text file\n");
			thisConv = GenericEntry::kConvertEOLOn;
		} else {
			ASSERT(thisConv == GenericEntry::kConvertEOLOff);
		}
	}
	if (thisConv != GenericEntry::kConvertEOLOff &&
		(thread == GenericEntry::kRsrcThread ||
		 thread == GenericEntry::kDiskImageThread))
	{
		WMSG0("Disabling EOL conv for resource fork or disk image\n");
		thisConv = GenericEntry::kConvertEOLOff;
	}


	/*
	 * Extract the contents to the file.
	 *
	 * In some cases, notably when the file size exceeds the limit of
	 * the reformatter, we will be trying to reformat but won't have
	 * loaded the original data.  In such cases we fall through to the
	 * normal extraction mode, because we threw out pOutput above when
	 * the result was kOutputErrorMsg.
	 *
	 * (Could also be due to extraction failure, e.g. bad CRC.)
	 */
	if (pOutput != nil) {
		/*
		 * We have the data in our buffer.  Write it out.  No need
		 * to tweak the progress updater, which already shows 100%.
		 *
		 * There are four possibilities:
		 *	- Valid text/rtf/csv converted text.  Write reformatted.
		 *	- Valid bitmap converted.  Write bitmap.
		 *	- No reformatter found, type is "raw".  Write raw.  (Note
		 *    this may be zero bytes long for an empty file.)
		 *	- Error message encoded in result.  Should not be here!
		 */
		if (pOutput->GetOutputKind() == ReformatOutput::kOutputText ||
			pOutput->GetOutputKind() == ReformatOutput::kOutputRTF ||
			pOutput->GetOutputKind() == ReformatOutput::kOutputCSV)
		{
			WMSG0("  Writing text, RTF, CSV, or raw\n");
			ASSERT(pOutput->GetTextBuf() != nil);
			int err = 0;
			if (fwrite(pOutput->GetTextBuf(),
					   pOutput->GetTextLen(), 1, fp) != 1)
				err = errno;
			if (err != 0) {
				errMsg.Format("Unable to save reformatted file '%s': %s\n",
					outputPath, strerror(err));
				fpActionProgress->MessageBox(errMsg, failed,
					MB_OK | MB_ICONERROR);
				writeFailed = true;
			} else {
				SET_PROGRESS_UPDATE(100);
			}
		} else if (pOutput->GetOutputKind() == ReformatOutput::kOutputBitmap) {
			WMSG0("  Writing bitmap\n");
			ASSERT(pOutput->GetDIB() != nil);
			int err = pOutput->GetDIB()->WriteToFile(fp);
			if (err != 0) {
				errMsg.Format("Unable to save bitmap '%s': %s\n",
					outputPath, strerror(err));
				fpActionProgress->MessageBox(errMsg, failed,
					MB_OK | MB_ICONERROR);
				writeFailed = true;
			} else {
				SET_PROGRESS_UPDATE(100);
			}
		} else if (pOutput->GetOutputKind() == ReformatOutput::kOutputRaw) {
			/*
			 * Send raw data through the text conversion configured in the
			 * extract files dialog.  Any file for which no dedicated
			 * reformatter could be found ends up here.
			 *
			 * We could just send it through to the generic non-reformatter
			 * case, but that would require reading the file twice.
			 */
			WMSG1("  Writing un-reformatted data (%ld bytes)\n",
				pOutput->GetTextLen());
			ASSERT(pOutput->GetTextBuf() != nil);
			bool lastCR = false;
			GenericEntry::ConvertHighASCII thisConvHA = convHA;
			int err;
			err = GenericEntry::WriteConvert(fp, pOutput->GetTextBuf(),
					pOutput->GetTextLen(), &thisConv, &thisConvHA, &lastCR);
			if (err != 0) {
				errMsg.Format("Unable to write file '%s': %s\n",
					outputPath, strerror(err));
				fpActionProgress->MessageBox(errMsg, failed,
					MB_OK | MB_ICONERROR);
				writeFailed = true;
			} else {
				SET_PROGRESS_UPDATE(100);
			}

		} else {
			/* something failed, and we don't have the file */
			WMSG0("How'd we get here?\n");
			ASSERT(false);
		}
	} else {
		/*
		 * We don't have the data, probably because we aren't using file
		 * reformatters.  Use the GenericEntry extraction routine to copy
		 * the data directly into the file.
		 *
		 * We also get here if the file has a length of zero.
		 */
		CString msg;
		int result;
		ASSERT(fpActionProgress != nil);
		WMSG3("Extracting '%s', requesting thisConv=%d, convHA=%d\n",
			outputPath, thisConv, convHA);
		result = pEntry->ExtractThreadToFile(thread, fp,
					thisConv, convHA, &msg);
		if (result != IDOK) {
			if (result == IDCANCEL) {
				CString msg;
				msg.LoadString(IDS_OPERATION_CANCELLED);
				fpActionProgress->MessageBox(msg, 
					"CiderPress", MB_OK | MB_ICONEXCLAMATION);
			} else {
				WMSG2("  FAILED on '%s': %s\n", outputPath, msg);
				errMsg.Format("Unable to extract file '%s': %s\n",
					outputPath, (LPCTSTR) msg);
				fpActionProgress->MessageBox(errMsg, failed,
					MB_OK | MB_ICONERROR);
			}
			writeFailed = true;
		}
	}

open_file_fail:
	delete pOutput;

	fclose(fp);
	if (writeFailed) {
		// clean up
		::DeleteFile(outputPath);
		return false;
	}

	/*
	 * Fix the modification date.
	 */
	PathName datePath(outputPath);
	datePath.SetModWhen(pEntry->GetModWhen());
//	datePath.SetAccess(pEntry->GetAccess());

	return true;
}


/*
 * Open an output file.
 *
 * "outputPath" holds the name of the file to create.  "origPath" is the
 * name as it was stored in the archive.  "pOverwriteExisting" tells us
 * if we should just go ahead and overwrite the existing file, while
 * "pOvwrForAll" tells us if a "To All" button was hit previously.
 *
 * If the file exists, "*pOverwriteExisting" is false, and "*pOvwrForAll"
 * is false, then we will put up the "do you want to overwrite?" dialog.
 * One possible outcome of the dialog is renaming the output path.
 *
 * On success, "*pFp" will be non-nil, and IDOK will be returned.  On
 * failure, IDCANCEL will be returned.  The values in "*pOverwriteExisting"
 * and "*pOvwrForAll" may be updated, and "*pOutputPath" will change if
 * the user chose to rename the file.
 */
int
MainWindow::OpenOutputFile(CString* pOutputPath, const PathProposal& pathProp,
	time_t arcFileModWhen, bool* pOverwriteExisting, bool* pOvwrForAll,
	FILE** pFp)
{
	const int kUserCancel = -2;		// must not conflict with errno values
	CString failed;
	CString msg;
	int err = 0;

	failed.LoadString(IDS_FAILED);

	*pFp = nil;

did_rename:
	PathName path(*pOutputPath);
	if (path.Exists()) {
		if (*pOverwriteExisting) {
do_overwrite:
			/* delete existing */
			WMSG1("  Deleting existing '%s'\n", (LPCTSTR) *pOutputPath);
			if (::unlink(*pOutputPath) != 0) {
				err = errno;
				WMSG2("  Failed deleting '%s', err=%d\n",
					(LPCTSTR)*pOutputPath, err);
				if (err == ENOENT) {
					/* user might have removed it while dialog was up */
					err = 0;
				} else {
					/* unable to delete, we'd better bail out */
					goto bail;
				}
			}
		} else if (*pOvwrForAll) {
			/* never overwrite */
			WMSG1("  Skipping '%s'\n", (LPCTSTR) *pOutputPath);
			goto bail;
		} else {
			/* no firm policy, ask the user */
			ConfirmOverwriteDialog confOvwr;
			PathName path(*pOutputPath);
			
			confOvwr.fExistingFile = *pOutputPath;
			confOvwr.fExistingFileModWhen = path.GetModWhen();
			confOvwr.fNewFileSource = pathProp.fStoredPathName;
			confOvwr.fNewFileModWhen = arcFileModWhen;
			if (confOvwr.DoModal() == IDCANCEL) {
				err = kUserCancel;
				goto bail;
			}
			if (confOvwr.fResultRename) {
				*pOutputPath = confOvwr.fExistingFile;
				goto did_rename;
			}
			if (confOvwr.fResultApplyToAll) {
				*pOvwrForAll = confOvwr.fResultApplyToAll;
				*pOverwriteExisting = confOvwr.fResultOverwrite;
			}
			if (confOvwr.fResultOverwrite)
				goto do_overwrite;
			else
				goto bail;
		}

	}

	/* create the subdirectories, if necessary */
	err = path.CreatePathIFN();
	if (err != 0)
		goto bail;

	*pFp = fopen(*pOutputPath, "wb");
	if (*pFp == nil)
		err = errno ? errno : -1;
	/* fall through with error */

bail:
	/* if we failed, tell the user why */
	if (err == ENOTDIR) {
		/* part of the output path exists, but isn't a directory */
		msg.Format("Unable to create folders for '%s': part of the path "
				   "already exists but is not a folder.\n",
			*pOutputPath);
		fpActionProgress->MessageBox(msg, failed, MB_OK | MB_ICONERROR);
		return IDCANCEL;
	} else if (err == EINVAL) {
		/* invalid argument; assume it's an invalid filename */
		msg.Format("Unable to create file '%s': invalid filename.\n",
			*pOutputPath);
		fpActionProgress->MessageBox(msg, failed, MB_OK | MB_ICONERROR);
		return IDCANCEL;
	} else if (err == kUserCancel) {
		/* user elected to cancel */
		WMSG0("Cancelling due to user request\n");
		return IDCANCEL;
	} else if (err != 0) {
		msg.Format("Unable to create file '%s': %s\n",
			*pOutputPath, strerror(err));
		fpActionProgress->MessageBox(msg, failed, MB_OK | MB_ICONERROR);
		return IDCANCEL;
	}

	return IDOK;
}


/*
 * ==========================================================================
 *		Test
 * ==========================================================================
 */

/*
 * Test files.
 */
void
MainWindow::OnActionsTest(void)
{
	ASSERT(fpContentList != nil);
	ASSERT(fpOpenArchive != nil);

	/*
	 * Ask the user about various options.
	 */
	UseSelectionDialog selOpts(fpContentList->GetSelectedCount(), this);
	selOpts.Setup(IDS_TEST_TITLE, IDS_TEST_OK, IDS_TEST_SELECTED_COUNT,
		IDS_TEST_SELECTED_COUNTS_FMT, IDS_TEST_ALL_FILES);
	if (fpContentList->GetSelectedCount() > 0)
		selOpts.fFilesToAction = UseSelectionDialog::kActionSelection;
	else
		selOpts.fFilesToAction = UseSelectionDialog::kActionAll;

	if (selOpts.DoModal() != IDOK) {
		WMSG0("Test cancelled\n");
		return;
	}

	/*
	 * Create a "selection set" of things to test.
	 *
	 * We don't currently test directories, because we don't currently
	 * allow testing anything that has a directory (NuFX doesn't store
	 * them explicitly).  We could probably add them to the threadMask.
	 */
	SelectionSet selSet;
	int threadMask = GenericEntry::kAnyThread;

	if (selOpts.fFilesToAction == UseSelectionDialog::kActionSelection) {
		selSet.CreateFromSelection(fpContentList, threadMask);
	} else {
		selSet.CreateFromAll(fpContentList, threadMask);
	}
	//selSet.Dump();

	if (selSet.GetNumEntries() == 0) {
		/* should be impossible */
		MessageBox("No files matched the selection criteria.",
			"No match", MB_OK|MB_ICONEXCLAMATION);
		return;
	}

	/*
	 * Set up the progress window.
	 */
	bool result;

	fpActionProgress = new ActionProgressDialog;
	fpActionProgress->Create(ActionProgressDialog::kActionTest, this);

	result = fpOpenArchive->TestSelection(fpActionProgress, &selSet);

	fpActionProgress->Cleanup(this);
	fpActionProgress = nil;
	//if (result)
	//	SuccessBeep();
}
void
MainWindow::OnUpdateActionsTest(CCmdUI* pCmdUI)
{
	pCmdUI->Enable(fpContentList != nil && fpContentList->GetItemCount() > 0
		&& fpOpenArchive->GetCapability(GenericArchive::kCapCanTest));
}


/*
 * ==========================================================================
 *		Delete
 * ==========================================================================
 */

/*
 * Delete archive entries.
 */
void
MainWindow::OnActionsDelete(void)
{
	ASSERT(fpContentList != nil);
	ASSERT(fpOpenArchive != nil);
	ASSERT(!fpOpenArchive->IsReadOnly());

	/*
	 * We handle deletions specially.  If they have selected any
	 * subdirectories, we recursively select the files in those subdirs
	 * as well.  We want to do it early so that the "#of files to delete"
	 * display accurately reflects what we're about to do.
	 */
	fpContentList->SelectSubdirContents();

#if 0
	/*
	 * Ask the user about various options.
	 */
	UseSelectionDialog delOpts(fpContentList->GetSelectedCount(), this);
	delOpts.Setup(IDS_DEL_TITLE, IDS_DEL_OK, IDS_DEL_SELECTED_COUNT,
		IDS_DEL_SELECTED_COUNTS_FMT, IDS_DEL_ALL_FILES);
	if (fpContentList->GetSelectedCount() > 0)
		delOpts.fFilesToAction = UseSelectionDialog::kActionSelection;
	else
		delOpts.fFilesToAction = UseSelectionDialog::kActionAll;

	if (delOpts.DoModal() != IDOK) {
		WMSG0("Delete cancelled\n");
		return;
	}
#endif

	/*
	 * Create a "selection set" of things to delete.
	 *
	 * We can't delete volume directories, so they're not included.
	 */
	SelectionSet selSet;
	int threadMask = GenericEntry::kAnyThread |
		GenericEntry::kAllowDirectory /*| GenericEntry::kAllowVolumeDir*/;

#if 0
	if (delOpts.fFilesToAction == UseSelectionDialog::kActionSelection) {
		selSet.CreateFromSelection(fpContentList, threadMask);
	} else {
		CString appName;
		UINT response;
		appName.LoadString(IDS_MB_APP_NAME);
		response = MessageBox("Are you sure you want to delete everything?",
			appName, MB_OKCANCEL | MB_ICONEXCLAMATION);
		if (response == IDCANCEL)
			return;
		selSet.CreateFromAll(fpContentList, threadMask);
	}
	//selSet.Dump();
#endif

	selSet.CreateFromSelection(fpContentList, threadMask);
	if (selSet.GetNumEntries() == 0) {
		/* can happen if they selected volume dir only */
		MessageBox("Nothing to delete.",
			"No match", MB_OK | MB_ICONEXCLAMATION);
		return;
	}

	CString appName, msg;

	appName.LoadString(IDS_MB_APP_NAME);
	msg.Format("Delete %d file%s?", selSet.GetNumEntries(),
		selSet.GetNumEntries() == 1 ? "" : "s");
	if (MessageBox(msg, appName, MB_OKCANCEL | MB_ICONQUESTION) != IDOK)
		return;

	bool result;
	fpActionProgress = new ActionProgressDialog;
	fpActionProgress->Create(ActionProgressDialog::kActionDelete, this);

	//fpContentList->Invalidate();		// don't allow updates until done
	result = fpOpenArchive->DeleteSelection(fpActionProgress, &selSet);
	fpContentList->Reload();

	fpActionProgress->Cleanup(this);
	fpActionProgress = nil;

	if (result)
		SuccessBeep();
}
void
MainWindow::OnUpdateActionsDelete(CCmdUI* pCmdUI)
{
	pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly()
		&& fpContentList->GetSelectedCount() > 0);
}


/*
 * ==========================================================================
 *		Rename
 * ==========================================================================
 */

/*
 * Rename archive entries.  Depending on the structure of the underlying
 * archive, we may only allow the user to alter the filename component.
 * Anything else would constitute moving the file around in the filesystem.
 */
void
MainWindow::OnActionsRename(void)
{
	ASSERT(fpContentList != nil);
	ASSERT(fpOpenArchive != nil);
	ASSERT(!fpOpenArchive->IsReadOnly());

	/*
	 * Create a "selection set" of entries to rename.  We always go by
	 * the selection, so there's no need to present a "all or some?" dialog.
	 *
	 * Renaming the volume dir is not done from here, so we don't include
	 * it in the set.  We could theoretically allow renaming of "damaged"
	 * files, since most of the time the damage is in the file structure
	 * not the directory, but the disk will be read-only anyway so there's
	 * no point.
	 */
	SelectionSet selSet;
	int threadMask = GenericEntry::kAnyThread | GenericEntry::kAllowDirectory;

	selSet.CreateFromSelection(fpContentList, threadMask);
	//selSet.Dump();

	if (selSet.GetNumEntries() == 0) {
		/* should be impossible */
		MessageBox("No files matched the selection criteria.",
			"No match", MB_OK | MB_ICONEXCLAMATION);
		return;
	}

	//fpContentList->Invalidate();	// this might be unnecessary
	fpOpenArchive->RenameSelection(this, &selSet);
	fpContentList->Reload();

	// user interaction on each step, so skip the SuccessBeep
}
void
MainWindow::OnUpdateActionsRename(CCmdUI* pCmdUI)
{
	pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly()
		&& fpContentList->GetSelectedCount() > 0);
}


/*
 * ==========================================================================
 *		Edit Comment
 * ==========================================================================
 */

/*
 * Edit a comment, creating it if necessary.
 */
void
MainWindow::OnActionsEditComment(void)
{
	ASSERT(fpContentList != nil);
	ASSERT(fpOpenArchive != nil);
	ASSERT(!fpOpenArchive->IsReadOnly());

	EditCommentDialog editDlg(this);
	CString oldComment;

	GenericEntry* pEntry = GetSelectedItem(fpContentList);
	if (pEntry == nil) {
		ASSERT(false);
		return;
	}

	if (!pEntry->GetHasComment()) {
		CString question, title;
		int result;

		question.LoadString(IDS_NO_COMMENT_ADD);
		title.LoadString(IDS_EDIT_COMMENT);
		result = MessageBox(question, title, MB_OKCANCEL | MB_ICONQUESTION);
		if (result == IDCANCEL)
			return;

		editDlg.fComment = "";
		editDlg.fNewComment = true;
	} else {
		fpOpenArchive->GetComment(this, pEntry, &editDlg.fComment);
	}


	int result;
	result = editDlg.DoModal();
	if (result == IDOK) {
		//fpContentList->Invalidate();	// probably unnecessary
		fpOpenArchive->SetComment(this, pEntry, editDlg.fComment);
		fpContentList->Reload();
	} else if (result == EditCommentDialog::kDeleteCommentID) {
		//fpContentList->Invalidate();	// possibly unnecessary
		fpOpenArchive->DeleteComment(this, pEntry);
		fpContentList->Reload();
	}
}
void
MainWindow::OnUpdateActionsEditComment(CCmdUI* pCmdUI)
{
	pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() &&
		fpContentList->GetSelectedCount() == 1 &&
		fpOpenArchive->GetCapability(GenericArchive::kCapCanEditComment));
}


/*
 * ==========================================================================
 *		Edit Properties
 * ==========================================================================
 */

/*
 * Edit file properties.
 *
 * This causes a reload of the list, which isn't really necessary.  We
 * do need to re-evaluate the sort order if one of the fields they modified
 * is the current sort key, but it would be nice if we could at least retain
 * the selection.  Since we're not reloading the GenericArchive, we *can*
 * remember the selection.
 */
void
MainWindow::OnActionsEditProps(void)
{
	ASSERT(fpContentList != nil);
	ASSERT(fpOpenArchive != nil);

	EditPropsDialog propsDlg(this);
	CString oldComment;

	GenericEntry* pEntry = GetSelectedItem(fpContentList);
	if (pEntry == nil) {
		ASSERT(false);
		return;
	}

	propsDlg.InitProps(pEntry);
	if (fpOpenArchive->IsReadOnly())
		propsDlg.fReadOnly = true;
	else if (pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir)
		propsDlg.fReadOnly = true;

	int result;
	result = propsDlg.DoModal();
	if (result == IDOK && !propsDlg.fReadOnly) {
		(void) fpOpenArchive->SetProps(this, pEntry, &propsDlg.fProps);

		// only needed if underlying archive reloads
		fpContentList->Reload(true);
	}
}
void
MainWindow::OnUpdateActionsEditProps(CCmdUI* pCmdUI)
{
	// allow it in read-only mode, so we can view the props
	pCmdUI->Enable(fpContentList != nil &&
		fpContentList->GetSelectedCount() == 1);
}


/*
 * ==========================================================================
 *		Rename Volume
 * ==========================================================================
 */

/*
 * Change a volume name or volume number.
 */
void
MainWindow::OnActionsRenameVolume(void)
{
	RenameVolumeDialog rvDialog;

	ASSERT(fpContentList != nil);
	ASSERT(fpOpenArchive != nil);
	ASSERT(!fpOpenArchive->IsReadOnly());

	/* only know how to deal with disk images */
	if (fpOpenArchive->GetArchiveKind() != GenericArchive::kArchiveDiskImage) {
		ASSERT(false);
		return;
	}

	DiskImgLib::DiskFS* pDiskFS;

	pDiskFS = ((DiskArchive*) fpOpenArchive)->GetDiskFS();
	ASSERT(pDiskFS != nil);

	rvDialog.fpArchive = (DiskArchive*) fpOpenArchive;
	if (rvDialog.DoModal() != IDOK)
		return;

	//WMSG1("Creating '%s'\n", rvDialog.fNewName);

	/* rename the chosen disk to the specified name */
	bool result;
	result = fpOpenArchive->RenameVolume(this, rvDialog.fpChosenDiskFS,
				rvDialog.fNewName);
	if (!result) {
		WMSG0("RenameVolume FAILED\n");
		/* keep going -- reload just in case something partially happened */
	}

	/*
	 * We need to do two things: reload the content list, because the
	 * underlying DiskArchive got reloaded, and update the title bar.  We
	 * put the "volume ID" in the title, and we most likely just changed it.
	 *
	 * SetCPTitle invokes fpOpenArchive->GetDescription(), which pulls the
	 * volume ID out of the primary DiskFS.
	 */
	fpContentList->Reload();
	SetCPTitle(fOpenArchivePathName, fpOpenArchive);
}
void
MainWindow::OnUpdateActionsRenameVolume(CCmdUI* pCmdUI)
{
	pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() &&
		fpOpenArchive->GetCapability(GenericArchive::kCapCanRenameVolume));
}


/*
 * ==========================================================================
 *		Recompress
 * ==========================================================================
 */

/*
 * Recompress files.
 */
void
MainWindow::OnActionsRecompress(void)
{
	ASSERT(fpContentList != nil);
	ASSERT(fpOpenArchive != nil);

	/*
	 * Ask the user about various options.
	 */
	RecompressOptionsDialog selOpts(fpContentList->GetSelectedCount(), this);
	selOpts.Setup(IDS_RECOMP_TITLE, IDS_RECOMP_OK, IDS_RECOMP_SELECTED_COUNT,
		IDS_RECOMP_SELECTED_COUNTS_FMT, IDS_RECOMP_ALL_FILES);
	if (fpContentList->GetSelectedCount() > 0)
		selOpts.fFilesToAction = UseSelectionDialog::kActionSelection;
	else
		selOpts.fFilesToAction = UseSelectionDialog::kActionAll;

	selOpts.fCompressionType = fPreferences.GetPrefLong(kPrCompressionType);

	if (selOpts.DoModal() != IDOK) {
		WMSG0("Recompress cancelled\n");
		return;
	}

	/*
	 * Create a "selection set" of data forks, resource forks, and disk
	 * images.  If an entry has nothing but a comment, ignore it.
	 */
	SelectionSet selSet;
	int threadMask = GenericEntry::kDataThread | GenericEntry::kRsrcThread |
		GenericEntry::kDiskImageThread;

	if (selOpts.fFilesToAction == UseSelectionDialog::kActionSelection) {
		selSet.CreateFromSelection(fpContentList, threadMask);
	} else {
		selSet.CreateFromAll(fpContentList, threadMask);
	}
	//selSet.Dump();

	if (selSet.GetNumEntries() == 0) {
		/* should be impossible */
		MessageBox("No files matched the selection criteria.",
			"No match", MB_OK|MB_ICONEXCLAMATION);
		return;
	}

	LONGLONG beforeUncomp, beforeComp;
	LONGLONG afterUncomp, afterComp;
	CalcTotalSize(&beforeUncomp, &beforeComp);

	/*
	 * Set up the progress window.
	 */
	int result;

	fpActionProgress = new ActionProgressDialog;
	fpActionProgress->Create(ActionProgressDialog::kActionRecompress, this);

	//fpContentList->Invalidate();		// possibly unnecessary
	result = fpOpenArchive->RecompressSelection(fpActionProgress, &selSet,
				&selOpts);
	fpContentList->Reload();

	fpActionProgress->Cleanup(this);
	fpActionProgress = nil;

	
	if (result) {
		CString msg, appName;

		CalcTotalSize(&afterUncomp, &afterComp);
		ASSERT(beforeUncomp == afterUncomp);

		appName.LoadString(IDS_MB_APP_NAME);
		msg.Format("Total uncompressed size of all files:\t%.1fK\r\n"
				   "Total size before recompress:\t\t%.1fK\r\n"
				   "Total size after recompress:\t\t%.1fK\r\n"
				   "Overall reduction:\t\t\t%.1fK",
			beforeUncomp / 1024.0, beforeComp / 1024.0, afterComp / 1024.0,
			(beforeComp - afterComp) / 1024.0);
		MessageBox(msg, appName, MB_OK|MB_ICONINFORMATION);
	}
}
void
MainWindow::OnUpdateActionsRecompress(CCmdUI* pCmdUI)
{
	pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() &&
		fpContentList->GetItemCount() > 0 &&
		fpOpenArchive->GetCapability(GenericArchive::kCapCanRecompress));
}

/*
 * Compute the total size of all files in the GenericArchive.
 */
void
MainWindow::CalcTotalSize(LONGLONG* pUncomp, LONGLONG* pComp) const
{
	GenericEntry* pEntry = fpOpenArchive->GetEntries();
	LONGLONG uncomp = 0, comp = 0;

	while (pEntry != nil) {
		uncomp += pEntry->GetUncompressedLen();
		comp += pEntry->GetCompressedLen();
		pEntry = pEntry->GetNext();
	}

	*pUncomp = uncomp;
	*pComp = comp;
}


/*
 * ==========================================================================
 *		Convert to disk archive
 * ==========================================================================
 */

/*
 * Select files to convert.
 */
void
MainWindow::OnActionsConvDisk(void)
{
	ASSERT(fpContentList != nil);
	ASSERT(fpOpenArchive != nil);

	/*
	 * Ask the user about various options.
	 */
	ConvDiskOptionsDialog selOpts(fpContentList->GetSelectedCount(), this);
	selOpts.Setup(IDS_CONVDISK_TITLE, IDS_CONVDISK_OK, IDS_CONVDISK_SELECTED_COUNT,
		IDS_CONVDISK_SELECTED_COUNTS_FMT, IDS_CONVDISK_ALL_FILES);
	if (fpContentList->GetSelectedCount() > 0)
		selOpts.fFilesToAction = UseSelectionDialog::kActionSelection;
	else
		selOpts.fFilesToAction = UseSelectionDialog::kActionAll;

	//selOpts.fAllowLower =
	//	fPreferences.GetPrefBool(kPrConvDiskAllowLower);
	//selOpts.fSparseAlloc =
	//	fPreferences.GetPrefBool(kPrConvDiskAllocSparse);

	if (selOpts.DoModal() != IDOK) {
		WMSG0("ConvDisk cancelled\n");
		return;
	}

	ASSERT(selOpts.fNumBlocks > 0);

	//fPreferences.SetPrefBool(kPrConvDiskAllowLower,
	//	selOpts.fAllowLower != 0);
	//fPreferences.SetPrefBool(kPrConvDiskAllocSparse,
	//	selOpts.fSparseAlloc != 0);

	/*
	 * Create a "selection set" of data forks, resource forks, and
	 * disk images.  We don't want comment threads, but we can ignore
	 * them later.
	 */
	SelectionSet selSet;
	int threadMask = GenericEntry::kAnyThread;

	if (selOpts.fFilesToAction == UseSelectionDialog::kActionSelection) {
		selSet.CreateFromSelection(fpContentList, threadMask);
	} else {
		selSet.CreateFromAll(fpContentList, threadMask);
	}
	//selSet.Dump();

	if (selSet.GetNumEntries() == 0) {
		/* should be impossible */
		MessageBox("No files matched the selection criteria.",
			"No match", MB_OK|MB_ICONEXCLAMATION);
		return;
	}

	XferFileOptions xferOpts;
	//xferOpts.fAllowLowerCase =
	//	fPreferences.GetPrefBool(kPrProDOSAllowLower) != 0;
	//xferOpts.fUseSparseBlocks =
	//	fPreferences.GetPrefBool(kPrProDOSUseSparse) != 0;

	WMSG1("New volume name will be '%s'\n", selOpts.fVolName);

	/*
	 * Create a new disk image.
	 */
	CString filename, saveFolder, errStr;

	CFileDialog dlg(FALSE, _T("po"), NULL,
		OFN_OVERWRITEPROMPT|OFN_NOREADONLYRETURN|OFN_HIDEREADONLY,
		"Disk Images (*.po)|*.po||", this);

	dlg.m_ofn.lpstrTitle = "New Disk Image (.PO)";
	dlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder);

	if (dlg.DoModal() != IDOK) {
		WMSG0(" User cancelled xfer from image create dialog\n");
		return;
	}

	saveFolder = dlg.m_ofn.lpstrFile;
	saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset);
	fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder);

	filename = dlg.GetPathName();
	WMSG1(" Will xfer to file '%s'\n", filename);

	/* remove file if it already exists */
	CString errMsg;
	errMsg = RemoveFile(filename);
	if (!errMsg.IsEmpty()) {
		ShowFailureMsg(this, errMsg, IDS_FAILED);
		return;
	}

	DiskArchive::NewOptions options;
	memset(&options, 0, sizeof(options));
	options.base.format = DiskImg::kFormatProDOS;
	options.base.sectorOrder = DiskImg::kSectorOrderProDOS;
	options.prodos.volName = selOpts.fVolName;
	options.prodos.numBlocks = selOpts.fNumBlocks;

	xferOpts.fTarget = new DiskArchive;
	errStr = xferOpts.fTarget->New(filename, &options);
	if (!errStr.IsEmpty()) {
		ShowFailureMsg(this, errStr, IDS_FAILED);
		delete xferOpts.fTarget;
		return;
	}

	/*
	 * Set up the progress window.
	 */
	GenericArchive::XferStatus result;

	fpActionProgress = new ActionProgressDialog;
	fpActionProgress->Create(ActionProgressDialog::kActionConvDisk, this);

	result = fpOpenArchive->XferSelection(fpActionProgress, &selSet,
				fpActionProgress, &xferOpts);

	fpActionProgress->Cleanup(this);
	fpActionProgress = nil;

	if (result == GenericArchive::kXferOK)
		SuccessBeep();

	/* clean up */
	delete xferOpts.fTarget;
}
void
MainWindow::OnUpdateActionsConvDisk(CCmdUI* pCmdUI)
{
	/* right now, only NufxArchive has the Xfer stuff implemented */
	pCmdUI->Enable(fpContentList != nil &&
		fpContentList->GetItemCount() > 0 &&
		fpOpenArchive->GetArchiveKind() == GenericArchive::kArchiveNuFX);
}


/*
 * ==========================================================================
 *		Convert to file archive
 * ==========================================================================
 */

/*
 * Select files to convert.
 */
void
MainWindow::OnActionsConvFile(void)
{
	ASSERT(fpContentList != nil);
	ASSERT(fpOpenArchive != nil);

	/*
	 * Ask the user about various options.
	 */
	ConvFileOptionsDialog selOpts(fpContentList->GetSelectedCount(), this);
	selOpts.Setup(IDS_CONVFILE_TITLE, IDS_CONVFILE_OK, IDS_CONVFILE_SELECTED_COUNT,
		IDS_CONVFILE_SELECTED_COUNTS_FMT, IDS_CONVFILE_ALL_FILES);
	if (fpContentList->GetSelectedCount() > 0)
		selOpts.fFilesToAction = UseSelectionDialog::kActionSelection;
	else
		selOpts.fFilesToAction = UseSelectionDialog::kActionAll;

	//selOpts.fConvDOSText =
	//	fPreferences.GetPrefBool(kPrConvFileConvDOSText);
	//selOpts.fConvPascalText =
	//	fPreferences.GetPrefBool(kPrConvFileConvPascalText);
	selOpts.fPreserveEmptyFolders =
		fPreferences.GetPrefBool(kPrConvFileEmptyFolders);

	if (selOpts.DoModal() != IDOK) {
		WMSG0("ConvFile cancelled\n");
		return;
	}

	//fPreferences.SetPrefBool(kPrConvFileConvDOSText,
	//	selOpts.fConvDOSText != 0);
	//fPreferences.SetPrefBool(kPrConvFileConvPascalText,
	//	selOpts.fConvPascalText != 0);
	fPreferences.SetPrefBool(kPrConvFileEmptyFolders,
		selOpts.fPreserveEmptyFolders != 0);

	/*
	 * Create a "selection set" of data forks, resource forks, and
	 * directories.  There are no comments or disk images on a disk image,
	 * so we just request "any" thread.
	 *
	 * We only need to explicitly include directories if "preserve
	 * empty folders" is set.
	 */
	SelectionSet selSet;
	int threadMask = GenericEntry::kAnyThread;
	if (selOpts.fPreserveEmptyFolders)
		threadMask |= GenericEntry::kAllowDirectory;

	if (selOpts.fFilesToAction == UseSelectionDialog::kActionSelection) {
		selSet.CreateFromSelection(fpContentList, threadMask);
	} else {
		selSet.CreateFromAll(fpContentList, threadMask);
	}
	//selSet.Dump();

	if (selSet.GetNumEntries() == 0) {
		MessageBox("No files matched the selection criteria.",
			"No match", MB_OK|MB_ICONEXCLAMATION);
		return;
	}

	XferFileOptions xferOpts;
	//xferOpts.fConvDOSText = (selOpts.fConvDOSText != 0);
	//xferOpts.fConvPascalText = (selOpts.fConvPascalText != 0);
	xferOpts.fPreserveEmptyFolders = (selOpts.fPreserveEmptyFolders != 0);

	/*
	 * Create a new NuFX archive.
	 */
	CString filename, saveFolder, errStr;

	CFileDialog dlg(FALSE, _T("shk"), NULL,
		OFN_OVERWRITEPROMPT|OFN_NOREADONLYRETURN|OFN_HIDEREADONLY,
		"ShrinkIt Archives (*.shk)|*.shk||", this);

	dlg.m_ofn.lpstrTitle = "New Archive";
	dlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder);

	if (dlg.DoModal() != IDOK) {
		WMSG0(" User cancelled xfer from archive create dialog\n");
		return;
	}

	saveFolder = dlg.m_ofn.lpstrFile;
	saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset);
	fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder);

	filename = dlg.GetPathName();
	WMSG1(" Will xfer to file '%s'\n", filename);

	/* remove file if it already exists */
	CString errMsg;
	errMsg = RemoveFile(filename);
	if (!errMsg.IsEmpty()) {
		ShowFailureMsg(this, errMsg, IDS_FAILED);
		return;
	}

	xferOpts.fTarget = new NufxArchive;
	errStr = xferOpts.fTarget->New(filename, nil);
	if (!errStr.IsEmpty()) {
		ShowFailureMsg(this, errStr, IDS_FAILED);
		delete xferOpts.fTarget;
		return;
	}

	/*
	 * Set up the progress window.
	 */
	GenericArchive::XferStatus result;

	fpActionProgress = new ActionProgressDialog;
	fpActionProgress->Create(ActionProgressDialog::kActionConvFile, this);

	result = fpOpenArchive->XferSelection(fpActionProgress, &selSet,
				fpActionProgress, &xferOpts);

	fpActionProgress->Cleanup(this);
	fpActionProgress = nil;
	if (result == GenericArchive::kXferOK)
		SuccessBeep();

	/* clean up */
	delete xferOpts.fTarget;
}
void
MainWindow::OnUpdateActionsConvFile(CCmdUI* pCmdUI)
{
	pCmdUI->Enable(fpContentList != nil &&
		fpContentList->GetItemCount() > 0 &&
		fpOpenArchive->GetArchiveKind() == GenericArchive::kArchiveDiskImage);
}


/*
 * ==========================================================================
 *		Cassette WAV conversions
 * ==========================================================================
 */

/*
 * Convert BAS, INT, or BIN to a cassette-audio WAV file.
 */
void
MainWindow::OnActionsConvToWav(void)
{
	// do this someday
	WMSG0("Convert TO wav\n");
}
void
MainWindow::OnUpdateActionsConvToWav(CCmdUI* pCmdUI)
{
	BOOL enable = false;

	if (fpContentList != nil && fpContentList->GetSelectedCount() == 1) {
		/* only BAS, INT, and BIN shorter than 64K */
		GenericEntry* pEntry = GetSelectedItem(fpContentList);

		if ((pEntry->GetFileType() == kFileTypeBAS ||
			 pEntry->GetFileType() == kFileTypeINT ||
			 pEntry->GetFileType() == kFileTypeBIN) &&
			pEntry->GetDataForkLen() < 65536 &&
			pEntry->GetRecordKind() == GenericEntry::kRecordKindFile)
		{
			enable = true;
		}
	}
	pCmdUI->Enable(enable);
}

/*
 * Convert a WAV file with a digitized Apple II cassette tape into an
 * Apple II file, and add it to the current disk.
 */
void
MainWindow::OnActionsConvFromWav(void)
{
	CassetteDialog dlg;
	CString fileName, saveFolder;

	CFileDialog fileDlg(TRUE, "wav", NULL, OFN_FILEMUSTEXIST|OFN_HIDEREADONLY,
		"Sound Files (*.wav)|*.wav||", this);
	fileDlg.m_ofn.lpstrTitle = "Open Sound File";
	fileDlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenWAVFolder);

	if (fileDlg.DoModal() != IDOK)
		goto bail;

	saveFolder = fileDlg.m_ofn.lpstrFile;
	saveFolder = saveFolder.Left(fileDlg.m_ofn.nFileOffset);
	fPreferences.SetPrefString(kPrOpenWAVFolder, saveFolder);

	fileName = fileDlg.GetPathName();
	WMSG1("Opening WAV file '%s'\n", fileName);

	dlg.fFileName = fileName;
	// pass in fpOpenArchive?

	dlg.DoModal();
	if (dlg.IsDirty()) {
		assert(fpContentList != nil);
		fpContentList->Reload();
	}

bail:
	return;
}
void
MainWindow::OnUpdateActionsConvFromWav(CCmdUI* pCmdUI)
{
	pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly());
}


/*
 * Utility function used by cassette import.
 *
 * May modify contents of "pDetails".
 *
 * On failure, returns with an error message in "errMsg".
 */
/*static*/ bool
MainWindow::SaveToArchive(GenericArchive::FileDetails* pDetails,
	const unsigned char* dataBufIn, long dataLen,
	const unsigned char* rsrcBufIn, long rsrcLen,
	CString& errMsg, CWnd* pDialog)
{
	MainWindow* pMain = GET_MAIN_WINDOW();
	GenericArchive* pArchive = pMain->GetOpenArchive();
	DiskImgLib::A2File* pTargetSubdir = nil;
	XferFileOptions xferOpts;
	CString storagePrefix;
	unsigned char* dataBuf = nil;
	unsigned char* rsrcBuf = nil;

	ASSERT(pArchive != nil);
	ASSERT(errMsg.IsEmpty());

	/*
	 * Make a copy of the data for XferFile.
	 */
	if (dataLen >= 0) {
		if (dataLen == 0)
			dataBuf = new unsigned char[1];
		else
			dataBuf = new unsigned char[dataLen];
		if (dataBuf == nil) {
			errMsg.Format("Unable to allocate %ld bytes", dataLen);
			goto bail;
		}
		memcpy(dataBuf, dataBufIn, dataLen);
	}
	if (rsrcLen >= 0) {
		assert(false);
	}


	/*
	 * Figure out where we want to put the files.  For a disk archive
	 * this can be complicated.
	 *
	 * The target DiskFS (which could be a sub-volume) gets tucked into
	 * the xferOpts.
	 */
	if (pArchive->GetArchiveKind() == GenericArchive::kArchiveDiskImage) {
		if (!pMain->ChooseAddTarget(&pTargetSubdir, &xferOpts.fpTargetFS))
			goto bail;
	} else if (pArchive->GetArchiveKind() == GenericArchive::kArchiveNuFX) {
		// Always use ':' separator for SHK; this is a matter of
		// convenience, so they can specify a full path.
		//details.storageName.Replace(':', '_');
		pDetails->fileSysInfo = ':';
	}
	if (pTargetSubdir != nil) {
		storagePrefix = pTargetSubdir->GetPathName();
		WMSG1("--- using storagePrefix '%s'\n", (const char*) storagePrefix);
	}
	if (!storagePrefix.IsEmpty()) {
		CString tmpStr, tmpFileName;
		tmpFileName = pDetails->storageName;
		tmpFileName.Replace(':', '_');	// strip any ':'s in the name
		pDetails->fileSysInfo = ':';
		tmpStr = storagePrefix;
		tmpStr += ':';
		tmpStr += tmpFileName;
		pDetails->storageName = tmpStr;
	}

	/*
	 * Handle the transfer.
	 *
	 * On success, XferFile will null out our dataBuf and rsrcBuf pointers.
	 */
	pArchive->XferPrepare(&xferOpts);

	errMsg = pArchive->XferFile(pDetails, &dataBuf, dataLen,
				&rsrcBuf, rsrcLen);
	delete[] dataBuf;
	delete[] rsrcBuf;

	if (errMsg.IsEmpty())
		pArchive->XferFinish(pDialog);
	else
		pArchive->XferAbort(pDialog);

bail:
	return (errMsg.IsEmpty() != 0);
}


/*
 * ==========================================================================
 *		Import BASIC programs from a text file
 * ==========================================================================
 */

/*
 * Import an Applesoft BASIC program from a text file.
 *
 * We currently allow the user to select a single file for import.  Someday
 * we may want to allow multi-file import.
 */
void
MainWindow::OnActionsImportBAS(void)
{
	ImportBASDialog dlg;
	CString fileName, saveFolder;

	CFileDialog fileDlg(TRUE, "txt", NULL, OFN_FILEMUSTEXIST | OFN_HIDEREADONLY,
		"Text files (*.txt)|*.txt||", this);
	fileDlg.m_ofn.lpstrTitle = "Open Text File";
	fileDlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrAddFileFolder);

	if (fileDlg.DoModal() != IDOK)
		goto bail;

	saveFolder = fileDlg.m_ofn.lpstrFile;
	saveFolder = saveFolder.Left(fileDlg.m_ofn.nFileOffset);
	fPreferences.SetPrefString(kPrAddFileFolder, saveFolder);

	fileName = fileDlg.GetPathName();
	WMSG1("Opening TXT file '%s'\n", fileName);

	dlg.fFileName = fileName;
	// pass in fpOpenArchive?

	dlg.DoModal();
	if (dlg.IsDirty()) {
		assert(fpContentList != nil);
		fpContentList->Reload();
	}

bail:
	return;
}
void
MainWindow::OnUpdateActionsImportBAS(CCmdUI* pCmdUI)
{
	pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly());
}


/*
 * ==========================================================================
 *		Multiple file handling
 * ==========================================================================
 */

/*
 * Extract every part of the file into "ReformatHolder".  Does not try to
 * reformat anything, just extracts the parts.
 *
 * Returns IDOK on success, IDCANCEL if the user cancelled, or -1 on error.
 * On error, the reformatted text buffer gets the error message.
 */
int
MainWindow::GetFileParts(const GenericEntry* pEntry,
	ReformatHolder** ppHolder) const
{
	ReformatHolder* pHolder = new ReformatHolder;
	CString errMsg;

	if (pHolder == nil)
		return -1;

	if (pEntry->GetHasDataFork())
		GetFilePart(pEntry, GenericEntry::kDataThread, pHolder);
	if (pEntry->GetHasRsrcFork())
		GetFilePart(pEntry, GenericEntry::kRsrcThread, pHolder);
	if (pEntry->GetHasComment())
		GetFilePart(pEntry, GenericEntry::kCommentThread, pHolder);
	if (pEntry->GetHasDiskImage())
		GetFilePart(pEntry, GenericEntry::kDiskImageThread, pHolder);

	*ppHolder = pHolder;

	return 0;
}

/*
 * Load the requested part.
 */
void
MainWindow::GetFilePart(const GenericEntry* pEntry, int whichThread,
	ReformatHolder* pHolder) const
{
	CString errMsg;
	ReformatHolder::ReformatPart part;
	char* buf = nil;
	long len = 0;
	di_off_t threadLen;
	int result;

	switch (whichThread) {
	case GenericEntry::kDataThread:
		part = ReformatHolder::kPartData;
		threadLen = pEntry->GetDataForkLen();
		break;
	case GenericEntry::kRsrcThread:
		part = ReformatHolder::kPartRsrc;
		threadLen = pEntry->GetRsrcForkLen();
		break;
	case GenericEntry::kCommentThread:
		part = ReformatHolder::kPartCmmt;
		threadLen = -1;		// no comment len getter; assume it's small
		break;
	case GenericEntry::kDiskImageThread:
		part = ReformatHolder::kPartData;		// put disks into data thread
		threadLen = pEntry->GetDataForkLen();
		break;
	default:
		ASSERT(false);
		return;
	}

	if (threadLen > fPreferences.GetPrefLong(kPrMaxViewFileSize)) {
		errMsg.Format(
			"[File size (%I64d KBytes) exceeds file viewer maximum (%ld KBytes).]\n",
			((LONGLONG) threadLen + 1023) / 1024,
			(fPreferences.GetPrefLong(kPrMaxViewFileSize) + 1023) / 1024);
		pHolder->SetErrorMsg(part, errMsg);
		goto bail;
	}
	

	result = pEntry->ExtractThreadToBuffer(whichThread, &buf, &len, &errMsg);

	if (result == IDOK) {
		/* on success, ETTB guarantees a buffer, even for zero-len file */
		ASSERT(buf != nil);
		pHolder->SetSourceBuf(part, (unsigned char*) buf, len);
	} else if (result == IDCANCEL) {
		/* not expected */
		errMsg = "Cancelled!";
		pHolder->SetErrorMsg(part, errMsg);
		ASSERT(buf == nil);
	} else {
		/* transfer error message to ReformatHolder buffer */
		WMSG1("Got error message from ExtractThread: '%s'\n", errMsg);
		pHolder->SetErrorMsg(part, errMsg);
		ASSERT(buf == nil);
	}

bail:
	return;
}
