/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * Handle clipboard functions (copy, paste).
 */
#include "StdAfx.h"
#include "Main.h"
#include "PasteSpecialDialog.h"


static const char* kClipboardFmtName = "faddenSoft:CiderPress:v1";
const int kClipVersion = 1;		// should match "vN" in fmt name
const unsigned short kEntrySignature = 0x4350;

/*
 * Win98 is quietly dying on large (20MB-ish) copies.  Everything
 * runs along nicely and then, the next time we try to interact with
 * Windows, the entire system locks up and must be Ctrl-Alt-Deleted.
 *
 * In tests, it blew up on 16762187 but not 16682322, both of which
 * are shy of the 16MB mark (the former by about 15K).  This includes the
 * text version, which is potentially copied multiple times.  Windows doesn't
 * create the additional stuff (CF_OEMTEXT and CF_LOCALE; Win2K also creates
 * CF_UNICODETEXT) until after the clipboard is closed.  We can open & close
 * the clipboard to get an exact count, or just multiply by a small integer
 * to get a reasonable estimate (1x for alternate text, 2x for UNICODE).
 *
 * Microsoft Excel limits its clipboard to 4MB on small systems, and 8MB
 * on systems with at least 64MB of physical memory.  My guess is they
 * haven't really tried larger values.
 *
 * The description of GlobalAlloc suggests that it gets a little weird
 * when you go above 4MB, which makes me a little nervous about using
 * anything larger.  However, it seems to work very reliably until you
 * cross 16MB, at which point it seizes up 100% of the time.  It's possible
 * we're stomping on stuff and just getting lucky, but it's too-reliably
 * successful and too-reliably failing for me to believe that.
 */
const long kWin98ClipboardMax = 16 * 1024 * 1024;
const long kWin98NeutralZone = 512;		// extra padding
const int kClipTextMult = 4;	// CF_OEMTEXT, CF_LOCALE, CF_UNICODETEXT*2

/*
 * File collection header.
 */
typedef struct FileCollection {
	unsigned short	version;		// currently 1
	unsigned short	dataOffset;		// offset to start of data
	unsigned long	length;			// total length;
	unsigned long	count;			// #of entries
} FileCollection;

/* what kind of entry is this */
typedef enum EntryKind {
	kEntryKindUnknown = 0,
	kEntryKindFileDataFork,
	kEntryKindFileRsrcFork,
	kEntryKindFileBothForks,
	kEntryKindDirectory,
	kEntryKindDiskImage,
} EntryKind;

/*
 * One of these per entry in the collection.
 *
 * The next file starts at (start + dataOffset + dataLen + rsrcLen + cmmtLen).
 */
typedef struct FileCollectionEntry {
	unsigned short	signature;		// let's be paranoid
	unsigned short	dataOffset;		// offset to start of data
	unsigned short	fileNameLen;	// len of filename
	unsigned long	dataLen;		// len of data fork
	unsigned long	rsrcLen;		// len of rsrc fork
	unsigned long	cmmtLen;		// len of comments
	unsigned long	fileType;
	unsigned long	auxType;
	time_t			createWhen;
	time_t			modWhen;
	unsigned char	access;			// ProDOS access flags
	unsigned char	entryKind;		// GenericArchive::FileDetails::FileKind
	unsigned char	sourceFS;		// DiskImgLib::DiskImg::FSFormat
	unsigned char	fssep;			// filesystem separator char, e.g. ':'

	/* data comes next: filename, then data, then resource, then comment */
} FileCollectionEntry;


/*
 * ==========================================================================
 *		Copy
 * ==========================================================================
 */

/*
 * Copy data to the clipboard.
 */
void
MainWindow::OnEditCopy(void)
{
	CString errStr, fileList;
	SelectionSet selSet;
	UINT myFormat;
	bool isOpen = false;
	HGLOBAL hGlobal;
	LPVOID pGlobal;
	unsigned char* buf = nil;
	long bufLen = -1;

	/* associate a number with the format name */
	myFormat = RegisterClipboardFormat(kClipboardFmtName);
	if (myFormat == 0) {
		errStr.LoadString(IDS_CLIPBOARD_REGFAILED);
		ShowFailureMsg(this, errStr, IDS_FAILED);
		goto bail;
	}
	WMSG1("myFormat = %u\n", myFormat);

	/* open & empty the clipboard, even if we fail later */
	if (OpenClipboard() == false) {
		errStr.LoadString(IDS_CLIPBOARD_OPENFAILED);
		ShowFailureMsg(this, errStr, IDS_FAILED);
		goto bail;
	}
	isOpen = true;
	EmptyClipboard();

	/*
	 * Create a selection set with the entries.
	 *
	 * Strictly speaking we don't need the directories, since we recreate
	 * them as needed.  However, storing them explicitly will allow us
	 * to preserve empty subdirs.
	 */
	selSet.CreateFromSelection(fpContentList,
		GenericEntry::kAnyThread | GenericEntry::kAllowDirectory);
	if (selSet.GetNumEntries() == 0) {
		errStr.LoadString(IDS_CLIPBOARD_NOITEMS);
		MessageBox(errStr, "No match", MB_OK | MB_ICONEXCLAMATION);
		goto bail;
	}

	/*
	 * Make a big string with a file listing.
	 */
	fileList = CreateFileList(&selSet);

	/*
	 * Add the string to the clipboard.  The clipboard will own the memory we
	 * allocate.
	 */
	hGlobal = ::GlobalAlloc(GHND | GMEM_SHARE, fileList.GetLength() +1);
	if (hGlobal == nil) {
		WMSG1("Failed allocating %ld bytes\n", fileList.GetLength() +1);
		errStr.LoadString(IDS_CLIPBOARD_ALLOCFAILED);
		ShowFailureMsg(this, errStr, IDS_FAILED);
		goto bail;
	}
	WMSG1("  Allocated %ld bytes for file list on clipboard\n",
		fileList.GetLength() +1);
	pGlobal = ::GlobalLock(hGlobal);
	ASSERT(pGlobal != nil);
	strcpy((char*) pGlobal, fileList);
	::GlobalUnlock(hGlobal);

	SetClipboardData(CF_TEXT, hGlobal);

	/*
	 * Create a (potentially very large) buffer with the contents of the
	 * files in it.  This may fail for any number of reasons.
	 */
	hGlobal = CreateFileCollection(&selSet);
	if (hGlobal != nil) {
		SetClipboardData(myFormat, hGlobal);
		// beep annoys me on copy
		//SuccessBeep();
	}

bail:
	CloseClipboard();
}
void
MainWindow::OnUpdateEditCopy(CCmdUI* pCmdUI)
{
	pCmdUI->Enable(fpContentList != nil &&
		fpContentList->GetSelectedCount() > 0);
}

/*
 * Create a list of selected files.
 *
 * The returned string uses tab-delineated fields with CSV-style quoting
 * around the filename (so that double quotes in the filename don't confuse
 * applications like MS Excel).
 */
CString
MainWindow::CreateFileList(SelectionSet* pSelSet)
{
	SelectionEntry* pSelEntry;
	GenericEntry* pEntry;
	CString tmpStr, fullStr;
	char fileTypeBuf[ContentList::kFileTypeBufLen];
	char auxTypeBuf[ContentList::kAuxTypeBufLen];
	CString fileName, subVol, fileType, auxType, modDate, format, length;

	pSelEntry = pSelSet->IterNext();
	while (pSelEntry != nil) {
		pEntry = pSelEntry->GetEntry();
		ASSERT(pEntry != nil);

		fileName = DblDblQuote(pEntry->GetPathName());
		subVol = pEntry->GetSubVolName();
		ContentList::MakeFileTypeDisplayString(pEntry, fileTypeBuf);
		fileType = DblDblQuote(fileTypeBuf);  // Mac HFS types might have '"'?
		ContentList::MakeAuxTypeDisplayString(pEntry, auxTypeBuf);
		auxType = DblDblQuote(auxTypeBuf);
		FormatDate(pEntry->GetModWhen(), &modDate);
		format = pEntry->GetFormatStr();
		length.Format("%I64d", (LONGLONG) pEntry->GetUncompressedLen());

		tmpStr.Format("\"%s\"\t%s\t\"%s\"\t\"%s\"\t%s\t%s\t%s\r\n",
			fileName, subVol, fileType, auxType, modDate, format, length);
		fullStr += tmpStr;

		pSelEntry = pSelSet->IterNext();
	}

	return fullStr;
}

/*
 * Double-up all double quotes.
 */
/*static*/ CString
MainWindow::DblDblQuote(const char* str)
{
	CString result;
	char* buf;

	buf = result.GetBuffer(strlen(str) * 2 +1);
	while (*str != '\0') {
		if (*str == '"') {
			*buf++ = *str;
			*buf++ = *str;
		} else {
			*buf++ = *str;
		}
		str++;
	}
	*buf = *str;

	result.ReleaseBuffer();

	return result;
}


/*
 * Compute the size of everything currently on the clipboard.
 */
long
MainWindow::GetClipboardContentLen(void)
{
	long len = 0;
	UINT format = 0;
	HGLOBAL hGlobal;

	while ((format = EnumClipboardFormats(format)) != 0) {
		hGlobal = GetClipboardData(format);
		ASSERT(hGlobal != nil);
		len += GlobalSize(hGlobal);
	}

	return len;
}

/*
 * Create the file collection.
 */
HGLOBAL
MainWindow::CreateFileCollection(SelectionSet* pSelSet)
{
	SelectionEntry* pSelEntry;
	GenericEntry* pEntry;
	HGLOBAL hGlobal = nil;
	HGLOBAL hResult = nil;
	LPVOID pGlobal;
	long totalLength, numFiles;
	long priorLength;

	/* get len of text version(s), with kluge to avoid close & reopen */
	priorLength = GetClipboardContentLen() * kClipTextMult;
	/* add some padding -- textmult doesn't work for fixed-size CF_LOCALE */
	priorLength += kWin98NeutralZone;

	totalLength = sizeof(FileCollection);
	numFiles = 0;

	/*
	 * Compute the amount of space required to hold it all.
	 */
	pSelSet->IterReset();
	pSelEntry = pSelSet->IterNext();
	while (pSelEntry != nil) {
		pEntry = pSelEntry->GetEntry();
		ASSERT(pEntry != nil);

		//WMSG1("+++ Examining '%s'\n", pEntry->GetDisplayName());

		if (pEntry->GetRecordKind() != GenericEntry::kRecordKindVolumeDir) {
			totalLength += sizeof(FileCollectionEntry);
			totalLength += strlen(pEntry->GetPathName()) +1;
			numFiles++;
			if (pEntry->GetRecordKind() != GenericEntry::kRecordKindDirectory) {
				totalLength += (long) pEntry->GetDataForkLen();
				totalLength += (long) pEntry->GetRsrcForkLen();
			}
		}

		if (totalLength < 0) {
			DebugBreak();
			WMSG0("Overflow\n");	// pretty hard to do right now!
			return nil;
		}

		pSelEntry = pSelSet->IterNext();
	}

#if 0
	{
		CString msg;
		msg.Format("totalLength is %ld+%ld = %ld",
			totalLength, priorLength, totalLength+priorLength);
		if (MessageBox(msg, nil, MB_OKCANCEL) == IDCANCEL)
			goto bail;
	}
#endif

	WMSG3("Total length required is %ld + %ld = %ld\n",
		totalLength, priorLength, totalLength+priorLength);
	if (IsWin9x() && totalLength+priorLength >= kWin98ClipboardMax) {
		CString errMsg;
		errMsg.Format(IDS_CLIPBOARD_WIN9XMAX,
			kWin98ClipboardMax / (1024*1024),
			((float) (totalLength+priorLength)) / (1024.0*1024.0));
		ShowFailureMsg(this, errMsg, IDS_MB_APP_NAME);
		goto bail;
	}

	/*
	 * Create a big buffer to hold it all.
	 */
	hGlobal = ::GlobalAlloc(GHND | GMEM_SHARE, totalLength);
	if (hGlobal == nil) {
		CString errMsg;
		errMsg.Format("ERROR: unable to allocate %ld bytes for copy",
			totalLength);
		WMSG1("%s\n", (const char*) errMsg);
		ShowFailureMsg(this, errMsg, IDS_FAILED);
		goto bail;
	}
	pGlobal = ::GlobalLock(hGlobal);

	ASSERT(pGlobal != nil);
	ASSERT(GlobalSize(hGlobal) >= (DWORD) totalLength);
	WMSG3("hGlobal=0x%08lx pGlobal=0x%08lx size=%ld\n",
		(long) hGlobal, (long) pGlobal, GlobalSize(hGlobal));

	/*
	 * Set up a progress dialog to track it.
	 */
	ASSERT(fpActionProgress == nil);
	fpActionProgress = new ActionProgressDialog;
	fpActionProgress->Create(ActionProgressDialog::kActionExtract, this);
	fpActionProgress->SetFileName("Clipboard");

	/*
	 * Extract the data into the buffer.
	 */
	long remainingLen;
	void* buf;

	remainingLen = totalLength - sizeof(FileCollection);
	buf = (unsigned char*) pGlobal + sizeof(FileCollection);
	pSelSet->IterReset();
	pSelEntry = pSelSet->IterNext();
	while (pSelEntry != nil) {
		CString errStr;

		pEntry = pSelEntry->GetEntry();
		ASSERT(pEntry != nil);

		fpActionProgress->SetArcName(pEntry->GetDisplayName());

		errStr = CopyToCollection(pEntry, &buf, &remainingLen);
		if (!errStr.IsEmpty()) {
			ShowFailureMsg(fpActionProgress, errStr, IDS_MB_APP_NAME);
			goto bail;
		}
		//WMSG1("remainingLen now %ld\n", remainingLen);

		pSelEntry = pSelSet->IterNext();
	}

	ASSERT(remainingLen == 0);
	ASSERT(buf == (unsigned char*) pGlobal + totalLength);

	/*
	 * Write the header.
	 */
	FileCollection fileColl;
	fileColl.version = kClipVersion;
	fileColl.dataOffset = sizeof(FileCollection);
	fileColl.length = totalLength;
	fileColl.count = numFiles;
	memcpy(pGlobal, &fileColl, sizeof(fileColl));

	/*
	 * Success!
	 */
	::GlobalUnlock(hGlobal);

	hResult = hGlobal;
	hGlobal = nil;

bail:
	if (hGlobal != nil) {
		ASSERT(hResult == nil);
		::GlobalUnlock(hGlobal);
		::GlobalFree(hGlobal);
	}
	if (fpActionProgress != nil) {
		fpActionProgress->Cleanup(this);
		fpActionProgress = nil;
	}
	return hResult;
}

/*
 * Copy the contents of the file referred to by "pEntry" into the buffer
 * "*pBuf", which has "*pBufLen" bytes in it.
 *
 * The call fails if "*pBufLen" isn't large enough.
 *
 * Returns an empty string on success, or an error message on failure.
 * On success, "*pBuf" will be advanced past the data added, and "*pBufLen"
 * will be reduced by the amount of data copied into "buf".
 */
CString
MainWindow::CopyToCollection(GenericEntry* pEntry, void** pBuf, long* pBufLen)
{
	FileCollectionEntry collEnt;
	CString errStr, dummyStr;
	unsigned char* buf = (unsigned char*) *pBuf;
	long remLen = *pBufLen;

	errStr.LoadString(IDS_CLIPBOARD_WRITEFAILURE);

	if (pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir) {
		WMSG0("Not copying volume dir to collection\n");
		return "";
	}

	if (remLen < sizeof(collEnt)) {
		ASSERT(false);
		return errStr;
	}

	GenericArchive::FileDetails::FileKind entryKind;
	if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory)
		entryKind = GenericArchive::FileDetails::kFileKindDirectory;
	else if (pEntry->GetHasDataFork() && pEntry->GetHasRsrcFork())
		entryKind = GenericArchive::FileDetails::kFileKindBothForks;
	else if (pEntry->GetHasDataFork())
		entryKind = GenericArchive::FileDetails::kFileKindDataFork;
	else if (pEntry->GetHasRsrcFork())
		entryKind = GenericArchive::FileDetails::kFileKindRsrcFork;
	else if (pEntry->GetHasDiskImage())
		entryKind = GenericArchive::FileDetails::kFileKindDiskImage;
	else {
		ASSERT(false);
		return errStr;
	}
	ASSERT((int) entryKind >= 0 && (int) entryKind <= 255);

	memset(&collEnt, 0x99, sizeof(collEnt));
	collEnt.signature = kEntrySignature;
	collEnt.dataOffset = sizeof(collEnt);
	collEnt.fileNameLen = strlen(pEntry->GetPathName()) +1;
	if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory) {
		collEnt.dataLen = collEnt.rsrcLen = collEnt.cmmtLen = 0;
	} else {
		collEnt.dataLen = (unsigned long) pEntry->GetDataForkLen();
		collEnt.rsrcLen = (unsigned long) pEntry->GetRsrcForkLen();
		collEnt.cmmtLen = 0;		// CMMT FIX -- length unknown??
	}
	collEnt.fileType = pEntry->GetFileType();
	collEnt.auxType = pEntry->GetAuxType();
	collEnt.createWhen = pEntry->GetCreateWhen();
	collEnt.modWhen = pEntry->GetModWhen();
	collEnt.access = (unsigned char) pEntry->GetAccess();
	collEnt.entryKind = (unsigned char) entryKind;
	collEnt.sourceFS = pEntry->GetSourceFS();
	collEnt.fssep = pEntry->GetFssep();

	/* verify there's enough space to hold everything */
	if ((unsigned long) remLen < collEnt.fileNameLen +
		collEnt.dataLen + collEnt.rsrcLen + collEnt.cmmtLen)
	{
		ASSERT(false);
		return errStr;
	}

	memcpy(buf, &collEnt, sizeof(collEnt));
	buf += sizeof(collEnt);
	remLen -= sizeof(collEnt);

	/* copy string with terminating null */
	memcpy(buf, pEntry->GetPathName(), collEnt.fileNameLen);
	buf += collEnt.fileNameLen;
	remLen -= collEnt.fileNameLen;

	/*
	 * Extract data forks, resource forks, and disk images as appropriate.
	 */
	char* bufCopy;
	long lenCopy;
	int result, which;

	if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory) {
		ASSERT(collEnt.dataLen == 0);
		ASSERT(collEnt.rsrcLen == 0);
		ASSERT(collEnt.cmmtLen == 0);
		ASSERT(!pEntry->GetHasRsrcFork());
	} else if (pEntry->GetHasDataFork() || pEntry->GetHasDiskImage()) {
		bufCopy = (char*) buf;
		lenCopy = remLen;
		if (pEntry->GetHasDiskImage())
			which = GenericEntry::kDiskImageThread;
		else
			which = GenericEntry::kDataThread;

		result = pEntry->ExtractThreadToBuffer(which, &bufCopy, &lenCopy,
					&dummyStr);
		if (result == IDCANCEL) {
			errStr.LoadString(IDS_CANCELLED);
			return errStr;
		} else if (result != IDOK) {
			WMSG0("ExtractThreadToBuffer (data) failed\n");
			return errStr;
		}

		ASSERT(lenCopy == (long) collEnt.dataLen);
		buf += collEnt.dataLen;
		remLen -= collEnt.dataLen;
	} else {
		ASSERT(collEnt.dataLen == 0);
	}

	if (pEntry->GetHasRsrcFork()) {
		bufCopy = (char*) buf;
		lenCopy = remLen;
		which = GenericEntry::kRsrcThread;

		result = pEntry->ExtractThreadToBuffer(which, &bufCopy, &lenCopy,
					&dummyStr);
		if (result == IDCANCEL) {
			errStr.LoadString(IDS_CANCELLED);
			return errStr;
		} else if (result != IDOK) {
			WMSG0("ExtractThreadToBuffer (rsrc) failed\n");
			return errStr;
		}

		ASSERT(lenCopy == (long) collEnt.rsrcLen);
		buf += collEnt.rsrcLen;
		remLen -= collEnt.rsrcLen;
	}

	if (pEntry->GetHasComment()) {
#if 0	// CMMT FIX
		bufCopy = (char*) buf;
		lenCopy = remLen;
		which = GenericEntry::kCommentThread;

		result = pEntry->ExtractThreadToBuffer(which, &bufCopy, &lenCopy);
		if (result == IDCANCEL) {
			errStr.LoadString(IDS_CANCELLED);
			return errStr;
		} else if (result != IDOK)
			return errStr;

		ASSERT(lenCopy == (long) collEnt.cmmtLen);
		buf += collEnt.cmmtLen;
		remLen -= collEnt.cmmtLen;
#else
		ASSERT(collEnt.cmmtLen == 0);
#endif
	}

	*pBuf = buf;
	*pBufLen = remLen;
	return "";
}


/*
 * ==========================================================================
 *		Paste
 * ==========================================================================
 */

/*
 * Paste data from the clipboard, using the configured defaults.
 */
void
MainWindow::OnEditPaste(void)
{
	bool pasteJunkPaths = fPreferences.GetPrefBool(kPrPasteJunkPaths);

	DoPaste(pasteJunkPaths);
}
void
MainWindow::OnUpdateEditPaste(CCmdUI* pCmdUI)
{
	bool dataAvailable = false;
	UINT myFormat;

	myFormat = RegisterClipboardFormat(kClipboardFmtName);
	if (myFormat != 0 && IsClipboardFormatAvailable(myFormat))
		dataAvailable = true;

	pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() &&
		dataAvailable);
}

/*
 * Paste data from the clipboard, giving the user the opportunity to select
 * how the files are handled.
 */
void
MainWindow::OnEditPasteSpecial(void)
{
	PasteSpecialDialog dlg;
	bool pasteJunkPaths = fPreferences.GetPrefBool(kPrPasteJunkPaths);

	// invert the meaning, so non-default mode is default in dialog
	if (pasteJunkPaths)
		dlg.fPasteHow = PasteSpecialDialog::kPastePaths;
	else
		dlg.fPasteHow = PasteSpecialDialog::kPasteNoPaths;
	if (dlg.DoModal() != IDOK)
		return;

	switch (dlg.fPasteHow) {
	case PasteSpecialDialog::kPastePaths:
		pasteJunkPaths = false;
		break;
	case PasteSpecialDialog::kPasteNoPaths:
		pasteJunkPaths = true;
		break;
	default:
		assert(false);
		break;
	}

	DoPaste(pasteJunkPaths);
}
void
MainWindow::OnUpdateEditPasteSpecial(CCmdUI* pCmdUI)
{
	OnUpdateEditPaste(pCmdUI);
}

/*
 * Do some prep work and then call ProcessClipboard to copy files in.
 */
void
MainWindow::DoPaste(bool pasteJunkPaths)
{
	CString errStr, buildStr;
	UINT format = 0;
	UINT myFormat;
	bool isOpen = false;

	if (fpContentList == nil || fpOpenArchive->IsReadOnly()) {
		ASSERT(false);
		return;
	}

	myFormat = RegisterClipboardFormat(kClipboardFmtName);
	if (myFormat == 0) {
		errStr.LoadString(IDS_CLIPBOARD_REGFAILED);
		ShowFailureMsg(this, errStr, IDS_FAILED);
		goto bail;
	}
	WMSG1("myFormat = %u\n", myFormat);

	if (OpenClipboard() == false) {
		errStr.LoadString(IDS_CLIPBOARD_OPENFAILED);
		ShowFailureMsg(this, errStr, IDS_FAILED);
		goto bail;;
	}
	isOpen = true;

	WMSG1("Found %d clipboard formats\n", CountClipboardFormats());
	while ((format = EnumClipboardFormats(format)) != 0) {
		CString tmpStr;
		tmpStr.Format(" %u", format);
		buildStr += tmpStr;
	}
	WMSG1("  %s\n", buildStr);

#if 0
	if (IsClipboardFormatAvailable(CF_HDROP)) {
		errStr.LoadString(IDS_CLIPBOARD_NO_HDROP);
		ShowFailureMsg(this, errStr, IDS_FAILED);
		goto bail;
	}
#endif

	/* impossible unless OnUpdateEditPaste was bypassed */
	if (!IsClipboardFormatAvailable(myFormat)) {
		errStr.LoadString(IDS_CLIPBOARD_NOTFOUND);
		ShowFailureMsg(this, errStr, IDS_FAILED);
		goto bail;
	}

	WMSG1("+++ total data on clipboard: %ld bytes\n",
		GetClipboardContentLen());

	HGLOBAL hGlobal;
	LPVOID pGlobal;

	hGlobal = GetClipboardData(myFormat);
	if (hGlobal == nil) {
		ASSERT(false);
		goto bail;
	}
	pGlobal = GlobalLock(hGlobal);
	ASSERT(pGlobal != nil);
	errStr = ProcessClipboard(pGlobal, GlobalSize(hGlobal), pasteJunkPaths);
	fpContentList->Reload();

	if (!errStr.IsEmpty())
		ShowFailureMsg(this, errStr, IDS_FAILED);
	else
		SuccessBeep();

	GlobalUnlock(hGlobal);

bail:
	if (isOpen)
		CloseClipboard();
}

/*
 * Process the data in the clipboard.
 *
 * Returns an empty string on success, or an error message on failure.
 */
CString
MainWindow::ProcessClipboard(const void* vbuf, long bufLen, bool pasteJunkPaths)
{
	FileCollection fileColl;
	CString errMsg, storagePrefix;
	const unsigned char* buf = (const unsigned char*) vbuf;
	DiskImgLib::A2File* pTargetSubdir = nil;
	XferFileOptions xferOpts;
	bool xferPrepped = false;

	/* set a standard error message */
	errMsg.LoadString(IDS_CLIPBOARD_READFAILURE);

	/*
	 * Pull the header out.
	 */
	if (bufLen < sizeof(fileColl)) {
		WMSG0("Clipboard contents too small!\n");
		goto bail;
	}
	memcpy(&fileColl, buf, sizeof(fileColl));

	/*
	 * Verify the length.  Win98 seems to like to round things up to 16-byte
	 * boundaries, which screws up our "bufLen > 0" while condition below.
	 */
	if ((long) fileColl.length > bufLen) {
		WMSG2("GLITCH: stored len=%ld, clip len=%ld\n",
			fileColl.length, bufLen);
		goto bail;
	}
	if (bufLen > (long) fileColl.length) {
		/* trim off extra */
		WMSG2("NOTE: Windows reports excess length (%ld vs %ld)\n",
			fileColl.length, bufLen);
		bufLen = fileColl.length;
	}

	buf += sizeof(fileColl);
	bufLen -= sizeof(fileColl);

	WMSG4("FileCollection found: vers=%d off=%d len=%ld count=%ld\n",
		fileColl.version, fileColl.dataOffset, fileColl.length,
		fileColl.count);
	if (fileColl.count == 0) {
		/* nothing to do? */
		ASSERT(false);
		return errMsg;
	}

	/*
	 * 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 (fpOpenArchive->GetArchiveKind() == GenericArchive::kArchiveDiskImage) {
		if (!ChooseAddTarget(&pTargetSubdir, &xferOpts.fpTargetFS))
			return "";
	}
	fpOpenArchive->XferPrepare(&xferOpts);
	xferPrepped = true;

	if (pTargetSubdir != nil) {
		storagePrefix = pTargetSubdir->GetPathName();
		WMSG1("--- using storagePrefix '%s'\n", (const char*) storagePrefix);
	}

	/*
	 * Set up a progress dialog to track it.
	 */
	ASSERT(fpActionProgress == nil);
	fpActionProgress = new ActionProgressDialog;
	fpActionProgress->Create(ActionProgressDialog::kActionAdd, this);
	fpActionProgress->SetArcName("Clipboard data");

	/*
	 * Loop over all files.
	 */
	WMSG1("+++ Starting paste, bufLen=%ld\n", bufLen);
	while (bufLen > 0) {
		FileCollectionEntry collEnt;
		CString fileName, processErrStr;

		/* read the entry info */
		if (bufLen < sizeof(collEnt)) {
			WMSG2("GLITCH: bufLen=%ld, sizeof(collEnt)=%d\n",
				bufLen, sizeof(collEnt));
			ASSERT(false);
			goto bail;
		}
		memcpy(&collEnt, buf, sizeof(collEnt));
		if (collEnt.signature != kEntrySignature) {
			ASSERT(false);
			goto bail;
		}

		/* advance to the start of data */
		if (bufLen < collEnt.dataOffset) {
			ASSERT(false);
			goto bail;
		}
		buf += collEnt.dataOffset;
		bufLen -= collEnt.dataOffset;

		/* extract the filename */
		if (bufLen < collEnt.fileNameLen) {
			ASSERT(false);
			goto bail;
		}
		fileName = buf;
		buf += collEnt.fileNameLen;
		bufLen -= collEnt.fileNameLen;

		//WMSG1("+++  pasting '%s'\n", fileName);

		/* strip the path (if requested) and prepend the storage prefix */
		ASSERT(fileName.GetLength() == collEnt.fileNameLen -1);
		if (pasteJunkPaths && collEnt.fssep != '\0') {
			int idx;
			idx = fileName.ReverseFind(collEnt.fssep);
			if (idx >= 0)
				fileName = fileName.Right(fileName.GetLength() - idx -1);
		}
		if (!storagePrefix.IsEmpty()) {
			CString tmpStr, tmpFileName;
			tmpFileName = fileName;
			if (collEnt.fssep == '\0') {
				tmpFileName.Replace(':', '_');	// strip any ':'s in the name
				collEnt.fssep = ':';			// define an fssep
			}

			tmpStr = storagePrefix;

			/* storagePrefix fssep is always ':'; change it to match */
			if (collEnt.fssep != ':')
				tmpStr.Replace(':', collEnt.fssep);

			tmpStr += collEnt.fssep;
			tmpStr += tmpFileName;
			fileName = tmpStr;

		}
		fpActionProgress->SetFileName(fileName);

		/* make sure the data is there */
		if (bufLen < (long) (collEnt.dataLen + collEnt.rsrcLen + collEnt.cmmtLen))
		{
			ASSERT(false);
			goto bail;
		}

		/*
		 * Process the entry.
		 *
		 * If the user hits "cancel" in the progress dialog we'll get thrown
		 * back out.  For the time being I'm just treating it like any other
		 * failure.
		 */
		processErrStr = ProcessClipboardEntry(&collEnt, fileName, buf, bufLen);
		if (!processErrStr.IsEmpty()) {
			errMsg.Format("Unable to paste '%s': %s.",
				(const char*) fileName, (const char*) processErrStr);
			goto bail;
		}
		
		buf += collEnt.dataLen + collEnt.rsrcLen + collEnt.cmmtLen;
		bufLen -= collEnt.dataLen + collEnt.rsrcLen + collEnt.cmmtLen;
	}

	ASSERT(bufLen == 0);
	errMsg = "";

bail:
	if (xferPrepped) {
		if (errMsg.IsEmpty())
			fpOpenArchive->XferFinish(this);
		else
			fpOpenArchive->XferAbort(this);
	}
	if (fpActionProgress != nil) {
		fpActionProgress->Cleanup(this);
		fpActionProgress = nil;
	}
	return errMsg;
}

/*
 * Process a single clipboard entry.
 *
 * On entry, "buf" points to the start of the first chunk of data (either
 * data fork or resource fork).  If the file has empty forks or is a
 * subdirectory, then "buf" is actually pointing at the start of the
 * next entry.
 */
CString
MainWindow::ProcessClipboardEntry(const FileCollectionEntry* pCollEnt,
	const char* pathName, const unsigned char* buf, long remLen)
{
	GenericArchive::FileDetails::FileKind entryKind;
	GenericArchive::FileDetails details;
	unsigned char* dataBuf = nil;
	unsigned char* rsrcBuf = nil;
	long dataLen, rsrcLen, cmmtLen;
	CString errMsg;

	entryKind = (GenericArchive::FileDetails::FileKind) pCollEnt->entryKind;
	WMSG2(" Processing '%s' (%d)\n", pathName, entryKind);

	details.entryKind = entryKind;
	details.origName = "Clipboard";
	details.storageName = pathName;
	details.fileSysFmt = (DiskImg::FSFormat) pCollEnt->sourceFS;
	details.fileSysInfo = pCollEnt->fssep;
	details.access = pCollEnt->access;
	details.fileType = pCollEnt->fileType;
	details.extraType = pCollEnt->auxType;
	GenericArchive::UNIXTimeToDateTime(&pCollEnt->createWhen,
		&details.createWhen);
	GenericArchive::UNIXTimeToDateTime(&pCollEnt->modWhen,
		&details.modWhen);
	time_t now = time(nil);
	GenericArchive::UNIXTimeToDateTime(&now, &details.archiveWhen);

	/*
	 * Because of the way XferFile works, we need to make a copy of
	 * the data.  (For NufxLib, it's going to gather up all of the
	 * data and flush it all at once, so it needs to own the memory.)
	 *
	 * Ideally we'd use a different interface that didn't require a
	 * data copy -- NufxLib can do it that way as well -- but it's
	 * not worth maintaining two separate interfaces.
	 *
	 * This approach does allow the xfer code to handle DOS high-ASCII
	 * text conversions in place, though.  If we didn't do it this way
	 * we'd have to make a copy in the xfer code to avoid contaminating
	 * the clipboard data.  That would be more efficient, but probably
	 * a bit messier.
	 *
	 * The stuff below figures out which forks we're expected to have based
	 * on the file type info.  This helps us distinguish between a file
	 * with a zero-length fork and a file without that kind of fork.
	 */
	bool hasData = false;
	bool hasRsrc = false;
	if (details.entryKind == GenericArchive::FileDetails::kFileKindDataFork) {
		hasData = true;
		details.storageType = kNuStorageSeedling;
	} else if (details.entryKind == GenericArchive::FileDetails::kFileKindRsrcFork) {
		hasRsrc = true;
		details.storageType = kNuStorageExtended;
	} else if (details.entryKind == GenericArchive::FileDetails::kFileKindBothForks) {
		hasData = hasRsrc = true;
		details.storageType = kNuStorageExtended;
	} else if (details.entryKind == GenericArchive::FileDetails::kFileKindDiskImage) {
		hasData = true;
		details.storageType = kNuStorageSeedling;
	} else if (details.entryKind == GenericArchive::FileDetails::kFileKindDirectory) {
		details.storageType = kNuStorageDirectory;
	} else {
		ASSERT(false);
		return "Internal error.";
	}

	if (hasData) {
		if (pCollEnt->dataLen == 0) {
			dataBuf = new unsigned char[1];
			dataLen = 0;
		} else {
			dataLen = pCollEnt->dataLen;
			dataBuf = new unsigned char[dataLen];
			if (dataBuf == nil)
				return "memory allocation failed.";
			memcpy(dataBuf, buf, dataLen);
			buf += dataLen;
			remLen -= dataLen;
		}
	} else {
		ASSERT(dataBuf == nil);
		dataLen = -1;
	}

	if (hasRsrc) {
		if (pCollEnt->rsrcLen == 0) {
			rsrcBuf = new unsigned char[1];
			rsrcLen = 0;
		} else {
			rsrcLen = pCollEnt->rsrcLen;
			rsrcBuf = new unsigned char[rsrcLen];
			if (rsrcBuf == nil)
				return "Memory allocation failed.";
			memcpy(rsrcBuf, buf, rsrcLen);
			buf += rsrcLen;
			remLen -= rsrcLen;
		}
	} else {
		ASSERT(rsrcBuf == nil);
		rsrcLen = -1;
	}

	if (pCollEnt->cmmtLen > 0) {
		cmmtLen = pCollEnt->cmmtLen;
		/* CMMT FIX -- not supported by XferFile */
	}

	ASSERT(remLen >= 0);

	errMsg = fpOpenArchive->XferFile(&details, &dataBuf, dataLen,
				&rsrcBuf, rsrcLen);
	delete[] dataBuf;
	delete[] rsrcBuf;
	dataBuf = rsrcBuf = nil;

	return errMsg;
}
