/*	SCCS Id: @(#)mrecover.c	3.2		96/07/24	  */
/*      Copyright (c) David Hairston, 1993.                       */
/* NetHack may be freely redistributed.  See license for details. */

/* Macintosh Recovery Application */

/* based on code in util/recover.c.  the significant differences are:
 * - GUI vs. CLI.  the vast majority of code here supports the GUI.
 * - Mac toolbox equivalents are used in place of ANSI functions.
 * - void restore_savefile(void) is event driven.
 * - integral type substitutions here and there.
 */

/*
 * Think C 5.0.4 project specs:
 * signature: 'nhRc'
 * SIZE (-1) info: flags: 0x5880, size: 65536L/65536L (64k/64k)
 * libraries: MacTraps [yes], MacTraps2 (HFileStuff) [yes], ANSI [no]
 * compatibility: system 6 and system 7
 * misc: sizeof(int): 2, "\p": unsigned char, enum size varies,
 *   prototypes required, type checking enforced, no optimizers,
 *   FAR CODE [no], FAR DATA [no], SEPARATE STRS [no], single segment,
 *   short macsbug symbols
 */
 
/*
 * To do (maybe, just maybe):
 * - Merge with the code in util/recover.c.
 * - Document launch (e.g. GUI equivalent of 'recover basename').
 * - Drag and drop.
 * - Internal memory tweaks (stack and heap usage).
 * - Use status file to allow resuming aborted recoveries.
 * - Bundle 'LEVL' files with recover (easier document launch).
 * - Prohibit recovering games "in progress".
 * - Share AppleEvents with NetHack to auto-recover crashed games.
 */


#include "config.h"

/**** Toolbox defines ****/

/* MPW C headers (99.44% pure) */
#include <Errors.h>
#include <OSUtils.h>
#include <Resources.h>
#include <Files.h>
#ifdef applec
#include <SysEqu.h>
#endif
#include <Menus.h>

#include <Desk.h>
#include <DiskInit.h>
#include <Notification.h>
#include <Packages.h>
#include <Script.h>
#include <StandardFile.h>
#include <ToolUtils.h>
#include <Processes.h>

#ifndef __MWERKS__	/* glue for System 7 Icon Family call (needed by Think C 5.0.4) */
pascal OSErr GetIconSuite(Handle *theIconSuite, short theResID, long selector)
	= {0x303C, 0x0501, 0xABC9};
#endif


/**** Application defines ****/

/* Memory */
typedef struct memBytes	/* format of 'memB' resource, preloaded/locked */
{
	short	memReserved;
	short	memCleanup;	/* 4   - memory monitor activity limit */
	long	memPreempt;	/* 32k - start iff FreeMem() > */
	long	memWarning;	/* 12k - warn if MaxMem() < */
	long	memAbort;	/* 4k  - abort if MaxMem() < */
	long	memIOBuf;	/* 16k - read/write buffer size */
} memBytes, *memBytesPtr, **memBytesHandle;

#define membID			128				/* 'memB' resource ID */


/* Cursor */
#define CURS_FRAME		4L				/* 1/15 second - spin cursor */
#define CURS_LATENT		60L				/* pause before spin cursor */
#define curs_Init		(-1)			/* token for set arrow */
#define curs_Total		8				/* maybe 'acur' would be better */
#define cursorOffset	128				/* GetCursor(cursorOffset + i) */


/* Menu */
enum
{
	mbar_Init = -1,
	mbarAppl,							/* normal mode */
	mbarRecover,						/* in recovery mode */
	mbarDA								/* DA in front mode */
};
enum
{
	menuApple,
	menuFile,
	menuEdit,
	menu_Total,

	muidApple = 128,
	muidFile,
	muidEdit
};
enum
{
	/* Apple menu */
	mitmAbout = 1,
	mitmHelp,
	____128_1,

	/* File menu */
	mitmOpen = 1,
	____129_1,
	mitmClose_DA,
	____129_2,
	mitmQuit

	/* standard minimum required Edit menu */
};


/* Alerts */
enum
{
	alrtNote,							/* general messages */
	alrtHelp,							/* help message */
	alrt_Total,

	alertAppleMenu = 127,				/* menuItem to alert ID offset */
	alidNote,
	alidHelp
};

#define aboutBufSize	80				/* i.e. 2 lines of 320 pixels */


/* Notification */
#define nmBufSize		(32 + aboutBufSize + 32)
typedef struct notifRec
{
	NMRec				nmr;
	struct notifRec	*	nmNext;
	short				nmDispose;
	unsigned char		nmBuf[nmBufSize];
} notifRec, *notifPtr;

#define nmPending		nmRefCon			/* &in.Notify */
#define iconNotifyID	128
#define ics_1_and_4		0x00000300

/* Dialogs */
enum
{
	dlogProgress = 256
};
enum
{
	uitmThermo = 1
};
enum
{
	initItem,
	invalItem,
	drawItem
};


/* Miscellaneous */
typedef struct modeFlags
{
	short	Front;			/* fg/bg event handling */
	short	Notify;			/* level of pending NM notifications */
	short	Dialog;			/* a modeless dialog is open */
	short	Recover;		/* restoration progress index */
} modeFlags;

/* convenient define to allow easier (for me) parsing of 'vers' resource */
typedef struct versXRec
{
	NumVersion		numVers;
	short			placeCode;
	unsigned char	versStr[];	/* (small string)(large string) */
} versXRec, *versXPtr, **versXHandle;



/**** Global variables ****/
modeFlags		in = {1};				/* in Front */
EventRecord		wnEvt;
SysEnvRec		sysEnv;
unsigned char	aboutBuf[aboutBufSize];	/* vers 1 "Get Info" string */
memBytesPtr		pBytes;					/* memory management */
unsigned short	memActivity;			/* more memory management */
MenuHandle		mHnd[menu_Total];
CursPtr			cPtr[curs_Total];		/* busy cursors */
unsigned long	timeCursor;				/* next cursor frame time */
short			oldCursor = curs_Init;	/* see adjustGUI() below */
notifPtr		pNMQ;					/* notification queue pointer */
notifRec		nmt;					/* notification template */
DialogTHndl		thermoTHnd;
DialogRecord	dlgThermo;				/* progress thermometer */
#define DLGTHM	((DialogPtr) &dlgThermo)
#define WNDTHM	((WindowPtr) &dlgThermo)
#define GRFTHM	((GrafPtr) &dlgThermo)

Point			sfGetWhere;				/* top left corner of get file dialog */
Ptr				pIOBuf;					/* read/write buffer pointer */
short			vRefNum;				/* SFGetFile working directory/volume refnum */
long			dirID;					/* directory i.d. */
NMUPP			nmCompletionUPP;		/* UPP for nmCompletion */
FileFilterUPP	basenameFileFilterUPP;	/* UPP for basenameFileFilter */

#define MAX_RECOVER_COUNT	256

#define APP_NAME_RES_ID		(-16396)	/* macfile.h */
#define PLAYER_NAME_RES_ID	1001		/* macfile.h */

/* variables from util/recover.c */
#define SAVESIZE	FILENAME
unsigned char	savename[SAVESIZE];		/* originally a C string */
unsigned char	lock[256];				/* pascal string */

int			hpid;					/* NetHack (unix-style) process i.d. */
short			saveRefNum;				/* save file descriptor */
short			gameRefNum;				/* level 0 file descriptor */
short			levRefNum;				/* level n file descriptor */


/**** Prototypes ****/
static	void warmup(void);
static	Handle alignTemplate(ResType, short, short, short, Point *);
pascal	void nmCompletion(NMRec *);
static	void noteErrorMessage(unsigned char *, unsigned char *);
static	void note(short, short, unsigned char *);
static	void adjustGUI(void);
static	void adjustMemory(void);
static	void optionMemStats(void);
static	void MenuEvent(long);
static	void eventLoop(void);
static	void cooldown(void);

pascal	void drawThermo(WindowPtr, short);
static	void itemizeThermo(short);
pascal	Boolean basenameFileFilter(ParmBlkPtr);
static	void beginRecover(void);
static	void continueRecover(void);
static	void endRecover(void);
static	short saveRezStrings(void);

/* analogous prototypes from util/recover.c */
static	void set_levelfile_name(long);
static	short open_levelfile(long);
static	short create_savefile(unsigned char *);
static	void copy_bytes(short, short);
static	void restore_savefile(void);

/* auxiliary prototypes */
static	long read_levelfile(short, Ptr, long);
static	long write_savefile(short, Ptr, long);
static	void close_file(short *);
static	void unlink_file(unsigned char *);


/**** Routines ****/

main()
{
	/* heap adjust */
	MaxApplZone();
	MoreMasters();
	MoreMasters();

	/* manager initialization */
	InitGraf(&qd.thePort);
	InitFonts();
	InitWindows();
	InitMenus();
	TEInit();
	InitDialogs(0L);
	InitCursor();
	nmCompletionUPP = NewNMProc(nmCompletion);
	basenameFileFilterUPP = NewFileFilterProc(basenameFileFilter);

	/* get system environment, notification requires 6.0 or better */
	(void) SysEnvirons(curSysEnvVers, &sysEnv);
	if (sysEnv.systemVersion < 0x0600)
	{
		ParamText("\pAbort: System 6.0 is required", "\p", "\p", "\p");
		(void) Alert(alidNote, (ModalFilterUPP) 0L);
		ExitToShell();
	}

	warmup();
	eventLoop();

	/* normally these routines are never reached from here */
	cooldown();
	ExitToShell();
}

static void
warmup()
{
	short		i;

	/* pre-System 7 MultiFinder hack for smooth launch */
	for (i = 0; i < 10; i++)
	{
		if (WaitNextEvent(osMask, &wnEvt, 2L, (RgnHandle) 0L))
			if (((wnEvt.message & osEvtMessageMask) >> 24) == suspendResumeMessage)
				in.Front = (wnEvt.message & resumeFlag);
	}

#if 0 // ???
	/* clear out the Finder info */
	{
		short	message, count;

		CountAppFiles(&message, &count);
		while(count)
			ClrAppFiles(count--);
	}
#endif

	/* fill out the notification template */
	nmt.nmr.qType = nmType;
	nmt.nmr.nmMark = 1;
	nmt.nmr.nmSound = (Handle) -1L;		/* system beep */
	nmt.nmr.nmStr = nmt.nmBuf;
	nmt.nmr.nmResp = nmCompletionUPP;
	nmt.nmr.nmPending = (long) &in.Notify;


#if 1
	{
		/* get the app name */
		ProcessInfoRec info;
		ProcessSerialNumber psn;

		info.processInfoLength = sizeof(info);
		info.processName = nmt.nmBuf;
		info.processAppSpec = NULL;
		GetCurrentProcess(&psn);
		GetProcessInformation(&psn, &info);
	}
#else
	/* prepend app name (31 chars or less) to notification buffer */
	{
		short	apRefNum;
		Handle	apParams;

		GetAppParms(* (Str255 *) &nmt.nmBuf, &apRefNum, &apParams);
	}
#endif

	/* add formatting (two line returns) */
	nmt.nmBuf[++(nmt.nmBuf[0])] = '\r';
	nmt.nmBuf[++(nmt.nmBuf[0])] = '\r';

	/**** note() is usable now but not aesthetically complete ****/

	/* get notification icon */
	if (sysEnv.systemVersion < 0x0700)
	{
		if (! (nmt.nmr.nmIcon = GetResource('SICN', iconNotifyID)))
			note(nilHandleErr, 0, "\pNil SICN Handle");
	}
	else
	{
		if (GetIconSuite(&nmt.nmr.nmIcon, iconNotifyID, ics_1_and_4))
			note(nilHandleErr, 0, "\pBad Icon Family");
	}

	/* load and align various dialog/alert templates */
	(void) alignTemplate('ALRT', alidNote, 0, 4, (Point *) 0L);
	(void) alignTemplate('ALRT', alidHelp, 0, 4, (Point *) 0L);

	thermoTHnd = (DialogTHndl) alignTemplate('DLOG', dlogProgress, 20, 8, (Point *) 0L);

	(void) alignTemplate('DLOG', getDlgID, 0, 6, (Point *) &sfGetWhere);

	/* get the "busy cursors" (preloaded/locked) */
	for (i = 0; i < curs_Total; i++)
	{
		CursHandle		cHnd;

		if (! (cHnd = GetCursor(i + cursorOffset)))
			note(nilHandleErr, 0, "\pNil CURS Handle");

		cPtr[i] = *cHnd;
	}

	/* get the 'vers' 1 long (Get Info) string - About Recover... */
	{
		versXHandle		vHnd;

		if (! (vHnd = (versXHandle) GetResource('vers', 1)))
			note(nilHandleErr, 0, "\pNil vers Handle");

		i = (**vHnd).versStr[0] + 1;		/* offset to Get Info pascal string */

		if ((aboutBuf[0] = (**vHnd).versStr[i]) > (aboutBufSize - 1))
			aboutBuf[0] = aboutBufSize - 1;

		i++;

		MoveHHi((Handle) vHnd);			/* DEE - Fense ... */
		HLock((Handle) vHnd);
		BlockMove(&((**vHnd).versStr[i]), &(aboutBuf[1]), aboutBuf[0]);
		ReleaseResource((Handle) vHnd);
	}

	/* form the menubar */
	for (i = 0; i < menu_Total; i++)
	{
		if (! (mHnd[i] = GetMenu(i + muidApple)))
			note(nilHandleErr, 0, "\pNil MENU Handle");

		/* expand the apple menu */
		if (i == menuApple)
			AddResMenu(mHnd[menuApple], 'DRVR');

		InsertMenu(mHnd[i], 0);
	}

	/* pre-emptive memory check */
	{
		memBytesHandle	hBytes;
		Size			grow;

		if (! (hBytes = (memBytesHandle) GetResource('memB', membID)))
			note(nilHandleErr, 0, "\pNil Memory Handle");

		pBytes = *hBytes;

		if (MaxMem(&grow) < pBytes->memPreempt)
			note(memFullErr, 0, "\pMore Memory Required\rTry adding 16k");

		memActivity = pBytes->memCleanup;		/* force initial cleanup */
	}

	/* get the I/O buffer */
	if (! (pIOBuf = NewPtr(pBytes->memIOBuf)))
		note(memFullErr, 0, "\pNil I/O Pointer");
}

/* align a window-related template to the main screen */
static Handle
alignTemplate(ResType rezType, short rezID, short vOff, short vDenom, Point *pPt)
{
	Handle	rtnHnd;
	Rect	*pRct;

	vOff += GetMBarHeight();

	if (! (rtnHnd = GetResource(rezType, rezID)))
		note(nilHandleErr, 0, "\pNil Template Handle");

	pRct = (Rect *) *rtnHnd;

	/* don't move memory while aligning rect */
	pRct->right -= pRct->left;		/* width */
	pRct->bottom -= pRct->top;		/* height */
	pRct->left = (qd.screenBits.bounds.right - pRct->right) / 2;
	pRct->top = (qd.screenBits.bounds.bottom - pRct->bottom - vOff) / vDenom;
	pRct->top += vOff;
	pRct->right += pRct->left;
	pRct->bottom += pRct->top;

	if (pPt)
		*pPt = * (Point *) pRct;	/* top left corner */

	return rtnHnd;
}

/* notification completion routine */
pascal void
nmCompletion(NMRec * pNMR)
{
	(void) NMRemove(pNMR);

	(* (short *) (pNMR->nmPending))--;	/* decrement pending note level */
	((notifPtr) pNMR)->nmDispose = 1;	/* allow DisposPtr() */
}

/*
 * handle errors inside of note().  the error message is appended to the
 * given message but on a separate line and must fit within nmBufSize.
 */
static void
noteErrorMessage(unsigned char *msg, unsigned char *errMsg)
{
	short	i = nmt.nmBuf[0] + 1;		/* insertion point */

	BlockMove(&msg[1], &nmt.nmBuf[i], msg[0]);
	nmt.nmBuf[i + msg[0]] = '\r';
	nmt.nmBuf[0] += (msg[0] + 1);

	note(memFullErr, 0, errMsg);
}

/*
 * display messages using Notification Manager or an alert.
 * no run-length checking is done.  the messages are created to fit
 * in the allocated space (nmBufSize and aboutBufSize).
 */
static void
note(short errorSignal, short alertID, unsigned char *msg)
{
	if (! errorSignal)
	{
		Size	grow;

		if (MaxMem(&grow) < pBytes->memAbort)
			noteErrorMessage(msg, "\pOut of Memory");
	}

	if (errorSignal || !in.Front)
	{
		notifPtr	pNMR;
		short		i = nmt.nmBuf[0] + 1;	/* insertion point */

		if (errorSignal)		/* use notification template */
		{
			pNMR = &nmt;

			/* we're going to abort so add in this prefix */
			BlockMove("Abort: ", &nmt.nmBuf[i], 7);
			i += 7;
			nmt.nmBuf[0] += 7;
		}
		else					/* allocate a notification record */
		{
			if (! (pNMR = (notifPtr) NewPtr(sizeof(notifRec))))
				noteErrorMessage(msg, "\pNil New Pointer");

			/* initialize it */
			*pNMR = nmt;
			pNMR->nmr.nmStr = (StringPtr) &(pNMR->nmBuf);

			/* update the notification queue */
			if (!pNMQ)
				pNMQ = pNMR;
			else
			{
				notifPtr	pNMX;

				/* find the end of the queue */
				for (pNMX = pNMQ; pNMX->nmNext; pNMX = pNMX->nmNext)
					;

				pNMX->nmNext = pNMR;
			}
		}

		/* concatenate the message */
		BlockMove(&msg[1], &((pNMR->nmBuf)[i]), msg[0]);
		(pNMR->nmBuf)[0] += msg[0];

		in.Notify++;			/* increase note pending level */

		NMInstall((NMRec *) pNMR);

		if (errorSignal)
			cooldown();

		return;
	}

	/* in front and no error so use an alert */
	ParamText(msg, "\p", "\p", "\p");
	(void) Alert(alertID, (ModalFilterUPP) 0L);
	ResetAlrtStage();

	memActivity++;
}

static void
adjustGUI()
{
	static short	oldMenubar = mbar_Init;	/* force initial update */
	short			newMenubar;
	WindowPeek		frontWindow;

	/* oldCursor is external so it can be reset in endRecover() */
	static short	newCursor = curs_Init;
	unsigned long	timeNow;
	short			useArrow;

	/* adjust menubar 1st */
	newMenubar = in.Recover ? mbarRecover : mbarAppl;

	/* desk accessories take precedence */
	if (frontWindow = (WindowPeek) FrontWindow())
		if (frontWindow->windowKind < 0)
			newMenubar = mbarDA;

	if (newMenubar != oldMenubar)
	{
		/* adjust menus */
		switch (oldMenubar = newMenubar)
		{
		case mbarAppl:
			EnableItem(mHnd[menuFile], mitmOpen);
			SetItemMark(mHnd[menuFile], mitmOpen, noMark);
			DisableItem(mHnd[menuFile], mitmClose_DA);
			DisableItem(mHnd[menuEdit], 0);
			break;

		case mbarRecover:
			DisableItem(mHnd[menuFile], mitmOpen);
			SetItemMark(mHnd[menuFile], mitmOpen, checkMark);
			DisableItem(mHnd[menuFile], mitmClose_DA);
			DisableItem(mHnd[menuEdit], 0);
			break;

		case mbarDA:
			DisableItem(mHnd[menuFile], mitmOpen);
			EnableItem(mHnd[menuFile], mitmClose_DA);
			EnableItem(mHnd[menuEdit], 0);
			break;
		}

		DrawMenuBar();
	}

	/* now adjust the cursor */
	if (useArrow = (!in.Recover || (newMenubar == mbarDA)))
		newCursor = curs_Init;
	else if ((timeNow = TickCount()) >= timeCursor)		/* spin cursor */
	{
		timeCursor = timeNow + CURS_FRAME;
		if (++newCursor >= curs_Total)
			newCursor = 0;
	}

	if (newCursor != oldCursor)
	{
		oldCursor = newCursor;

		SetCursor(useArrow ? &qd.arrow : cPtr[newCursor]);
	}
}

static void
adjustMemory()
{
	Size		grow;

	memActivity = 0;

	if (MaxMem(&grow) < pBytes->memWarning)
		note(noErr, alidNote, "\pWarning: Memory is running low");

	(void) ResrvMem((Size) FreeMem());		/* move all handles high */
}

/* show memory stats: FreeMem, MaxBlock, PurgeSpace, and StackSpace */
static void
optionMemStats()
{
	unsigned char	*pFormat = "\pFree:#k  Max:#k  Purge:#k  Stack:#k";
	char			*pSub = "#";		/* not a pascal string */
	unsigned char	nBuf[16];
	long			nStat, contig;
	Handle			strHnd;
	long			nOffset;
	short			i;

	if (wnEvt.modifiers & shiftKey)
		adjustMemory();

	if (! (strHnd = NewHandle((Size) 128)))
	{
		note(noErr, alidNote, "\pOops: Memory stats unavailable!");
		return;
	}
	
	SetString((StringHandle) strHnd, pFormat);
	nOffset = 1L;

	for (i = 1; i <= 4; i++)
	{
		/* get the replacement number stat */
		switch (i)
		{
		case 1: nStat = FreeMem();				break;
		case 2: nStat = MaxBlock();				break;
		case 3: PurgeSpace(&nStat, &contig);	break;
		case 4: nStat = StackSpace();			break;
		}

		NumToString((nStat >> 10), * (Str255 *) &nBuf);

		**strHnd += nBuf[0] - 1;
		nOffset = Munger(strHnd, nOffset, (Ptr) pSub, 1L, (Ptr) &nBuf[1], nBuf[0]);
	}

	MoveHHi(strHnd);
	HLock(strHnd);
	note(noErr, alidNote, (unsigned char *) *strHnd);
	DisposHandle(strHnd);
}

static void
MenuEvent(long menuEntry)
{
	short menuID = HiWord(menuEntry);
	short menuItem = LoWord(menuEntry);

	switch (menuID)
	{
	case muidApple:
		switch (menuItem)
		{
		case mitmAbout:
			if (wnEvt.modifiers & optionKey)
				optionMemStats();
			/* fall thru */
		case mitmHelp:
			note(noErr, (alertAppleMenu + menuItem), aboutBuf);
			break;

		default:	/* DA's or apple menu items */
			{
				unsigned char	daName[32];

				GetItem(mHnd[menuApple], menuItem, * (Str255 *) &daName);
				(void) OpenDeskAcc(daName);

				memActivity++;
			}
			break;
		}
		break;

	case muidFile:
		switch (menuItem)
		{
		case mitmOpen:
			beginRecover();
			break;

		case mitmClose_DA:
			{
				WindowPeek	frontWindow;
				short		refNum;

				if (frontWindow = (WindowPeek) FrontWindow())
					if ((refNum = frontWindow->windowKind) < 0)
						CloseDeskAcc(refNum);

				memActivity++;
			}
			break;

		case mitmQuit:
			cooldown();
			break;
		}
		break;

	case muidEdit:
		(void) SystemEdit(menuItem - 1);
		break;
	}

	HiliteMenu(0);
}

static void
eventLoop()
{
	short	wneMask = (in.Front ? everyEvent : (osMask + updateMask));
	long	wneSleep = (in.Front ? 0L : 3L);

	while (1)
	{
		if (in.Front)
			adjustGUI();

		if (memActivity >= pBytes->memCleanup)
			adjustMemory();

		(void) WaitNextEvent(wneMask, &wnEvt, wneSleep, (RgnHandle) 0L);

		if (in.Dialog)
			(void) IsDialogEvent(&wnEvt);

		switch (wnEvt.what)
		{
		case osEvt:
			if (((wnEvt.message & osEvtMessageMask) >> 24) == suspendResumeMessage)
			{
				in.Front = (wnEvt.message & resumeFlag);
				wneMask = (in.Front ? everyEvent : (osMask + updateMask));
				wneSleep = (in.Front ? 0L : 3L);
			}
			break;

		case nullEvent:
			/* adjust the FIFO notification queue */
			if (pNMQ && pNMQ->nmDispose)
			{
				notifPtr pNMX = pNMQ->nmNext;

				DisposPtr((Ptr) pNMQ);
				pNMQ = pNMX;

				memActivity++;
			}

			if (in.Recover)
				continueRecover();
			break;

		case mouseDown:
			{
				WindowPtr	whichWindow;
				
				switch(FindWindow( wnEvt . where , &whichWindow))
				{
				case inMenuBar:
					MenuEvent(MenuSelect( wnEvt . where ));
					break;

				case inSysWindow:
					SystemClick(&wnEvt, whichWindow);
					break;

				case inDrag:
					{
						Rect	boundsRect = qd.screenBits.bounds;
						Point	offsetPt;

						InsetRect(&boundsRect, 4, 4);
						boundsRect.top += GetMBarHeight();

						DragWindow(whichWindow, * ((Point *) &wnEvt.where), &boundsRect);

						boundsRect = whichWindow->portRect;
						offsetPt = * (Point *) &(whichWindow->portBits.bounds);
						OffsetRect(&boundsRect, -offsetPt.h, -offsetPt.v);

						* (Rect *) *thermoTHnd = boundsRect;
					}
					break;
				}
			}
			break;

		case keyDown:
			{
				char	key = (wnEvt.message & charCodeMask);

				if (wnEvt.modifiers & cmdKey)
				{
					if (key == '.')
					{
						if (in.Recover)
						{
							endRecover();
							note(noErr, alidNote, "\pSorry: Recovery aborted");
						}
					}
					else
						MenuEvent(MenuKey(key));
				}
			}
			break;

		/* without windows these events belong to our thermometer */
		case updateEvt:
		case activateEvt:
		{
			DialogPtr	dPtr;
			short		itemHit;

			(void) DialogSelect(&wnEvt, &dPtr, &itemHit);
		}

		case diskEvt:
			if (HiWord(wnEvt.message))
			{
				Point	pt = {60, 60};

				(void) DIBadMount(pt, wnEvt.message);
				DIUnload();

				memActivity++;
			}
			break;
		}			/* switch (wnEvt.what) */
	}				/* while (1) */
}

static void
cooldown()
{
	if (in.Recover)
		endRecover();

	/* wait for pending notifications to complete */
	while (in.Notify)
		(void) WaitNextEvent(0, &wnEvt, 3L, (RgnHandle) 0L);

	ExitToShell();
}

/* draw the progress thermometer and frame.  1 level <=> 1 horiz. pixel */
pascal void
drawThermo(WindowPtr wPtr, short inum)
{
	itemizeThermo(drawItem);
}

/* manage progress thermometer dialog */
static void
itemizeThermo(short itemMode)
{
	short	iTyp, iTmp;
	Handle	iHnd;
	Rect	iRct;

	GetDItem(DLGTHM, uitmThermo, &iTyp, &iHnd, &iRct);

	switch(itemMode)
	{
	case initItem:
		SetDItem(DLGTHM, uitmThermo, iTyp, (Handle) drawThermo, &iRct);
		break;

	case invalItem:
		{
			GrafPtr	oldPort;

			GetPort(&oldPort);
			SetPort(GRFTHM);

			InsetRect(&iRct, 1, 1);
			InvalRect(&iRct);

			SetPort(oldPort);
		}
		break;

	case drawItem:
			FrameRect(&iRct);
			InsetRect(&iRct, 1, 1);

			iTmp = iRct.right;
			iRct.right = iRct.left + in.Recover;
			PaintRect(&iRct);

			iRct.left = iRct.right;
			iRct.right = iTmp;
			EraseRect(&iRct);
		break;
	}
}

/* show only <pid-plname>.0 files in get file dialog */
pascal Boolean
basenameFileFilter(ParmBlkPtr pPB)
{
	unsigned char	*pC;

	if (! (pC = (unsigned char *) pPB->fileParam.ioNamePtr))
		return true;

	if ((*pC < 4) || (*pC > 28))						/* save/ 1name .0 */
		return true;

	if ((pC[*pC - 1] == '.') && (pC[*pC] == '0'))		/* bingo! */
		return false;

	return true;
}

static void
beginRecover()
{
	SFTypeList		levlType = {'LEVL'};
	SFReply			sfGetReply;

	SFGetFile(sfGetWhere, "\p", basenameFileFilterUPP, 1, levlType,
				(DlgHookUPP) 0L, &sfGetReply);

	memActivity++;

	if (! sfGetReply.good)
		return;

	/* get volume (working directory) refnum, basename, and directory i.d. */
	vRefNum = sfGetReply.vRefNum;
	BlockMove(sfGetReply.fName, lock, sfGetReply.fName[0] + 1);
	{
		static CInfoPBRec	catInfo;

		catInfo.hFileInfo.ioNamePtr = (StringPtr) sfGetReply.fName;
		catInfo.hFileInfo.ioVRefNum = sfGetReply.vRefNum;
		catInfo.hFileInfo.ioDirID = 0L;

		if (PBGetCatInfoSync(&catInfo))
		{
			note(noErr, alidNote, "\pSorry: Bad File Info");
			return;
		}

		dirID = catInfo.hFileInfo.ioFlParID;
	}

	/* open the progress thermometer dialog */
	(void) GetNewDialog(dlogProgress, (Ptr) &dlgThermo, (WindowPtr) -1L);
	if (ResError() || MemError())
		note(noErr, alidNote, "\pOops: Progress thermometer unavailable");
	else
	{
		in.Dialog = 1;
		memActivity++;

		itemizeThermo(initItem);

		ShowWindow(WNDTHM);
	}

	timeCursor = TickCount() + CURS_LATENT;
	saveRefNum = gameRefNum = levRefNum = -1;
	in.Recover = 1;
}

static void
continueRecover()
{
	restore_savefile();

	/* update the thermometer */
	if (in.Dialog && ! (in.Recover % 4))
		itemizeThermo(invalItem);

	if (in.Recover <= MAX_RECOVER_COUNT)
		return;

	endRecover();

	if (saveRezStrings())
		return;

	note(noErr, alidNote, "\pOK: Recovery succeeded");
}

/* no messages from here (since we might be quitting) */
static void
endRecover()
{
	in.Recover = 0;

	oldCursor = curs_Init;
	SetCursor(&qd.arrow);

	/* clean up abandoned files */
	if (gameRefNum >= 0)
		(void) FSClose(gameRefNum);

	if (levRefNum >= 0)
		(void) FSClose(levRefNum);

	if (saveRefNum >= 0)
	{
		(void) FSClose(saveRefNum);
		(void) FlushVol((StringPtr) 0L, vRefNum);
		/* its corrupted so trash it ... */
		(void) HDelete(vRefNum, dirID, savename);
	}

	saveRefNum = gameRefNum = levRefNum = -1;

	/* close the progress thermometer dialog */
	in.Dialog = 0;
	CloseDialog(DLGTHM);
	DisposHandle(dlgThermo.items);
	memActivity++;
}

/* add friendly, non-essential resource strings to save file */
static short
saveRezStrings()
{
	short			sRefNum;
	StringHandle	strHnd;
	short			i, rezID;
	unsigned char	*plName;

	HCreateResFile(vRefNum, dirID, savename);

	sRefNum = HOpenResFile(vRefNum, dirID, savename, fsRdWrPerm);
	if (sRefNum <= 0)
	{
		note(noErr, alidNote, "\pOK: Minor resource map error");
		return 1;
	}

	/* savename and hpid get mutilated here... */
	plName = savename + 5;				/* save/ */
	*savename -= 5;
	do
	{
		plName++;
		(*savename)--;
		hpid /= 10;
	}
	while (hpid);
	*plName = *savename;

	for (i = 1; i <= 2; i++)
	{
		switch (i)
		{
		case 1:
			rezID = PLAYER_NAME_RES_ID;
			strHnd = NewString(* (Str255 *) plName);
			break;

		case 2:
			rezID = APP_NAME_RES_ID;
			strHnd = NewString(* (Str255 *) "\pNetHack");
			break;
		}

		if (! strHnd)
		{
			note(noErr, alidNote, "\pOK: Minor \'STR \' resource error");
			CloseResFile(sRefNum);
			return 1;
		}

		/* should check for errors... */
		AddResource((Handle) strHnd, 'STR ', rezID, * (Str255 *) "\p");
	}

	memActivity++;

	/* should check for errors... */
	CloseResFile(sRefNum);
	return 0;
}

static void
set_levelfile_name(long lev)
{
	unsigned char	*tf;

	/* find the dot.  this is guaranteed to happen. */
	for (tf = (lock + *lock); *tf != '.'; tf--, lock[0]--)
		;

	/* append the level number string (pascal) */
	if (tf > lock)
	{
		NumToString(lev, * (Str255 *) tf);
		lock[0] += *tf;
		*tf = '.';
	}
	else	/* huh??? */
	{
		endRecover();
		note(noErr, alidNote, "\pSorry: File Name Error");
	}
}

static short
open_levelfile(long lev)
{
	OSErr	openErr;
	short	fRefNum;

	set_levelfile_name(lev);
	if (! in.Recover)
		return (-1);

	if ((openErr = HOpen(vRefNum, dirID, lock, fsRdWrPerm, &fRefNum))
			&& (openErr != fnfErr))
	{
		endRecover();
		note(noErr, alidNote, "\pSorry: File Open Error");
		return (-1);
	}

	return (openErr ? -1 : fRefNum);
}

static short
create_savefile(unsigned char *savename)
{
	short	fRefNum;

	/* translate savename to a pascal string (in place) */
	{
		unsigned char	*pC;
		short			nameLen;

		for (pC = savename; *pC; pC++);

		nameLen = pC - savename;

		for ( ; pC > savename; pC--)
			*pC = *(pC - 1);

		*savename = nameLen;
	}

	if (HCreate(vRefNum, dirID, savename, MAC_CREATOR, SAVE_TYPE)
		|| HOpen(vRefNum, dirID, savename, fsRdWrPerm, &fRefNum))
	{
		endRecover();
		note(noErr, alidNote, "\pSorry: File Create Error");
		return (-1);
	}

	return fRefNum;
}

static void
copy_bytes(short inRefNum, short outRefNum)
{
	char	*buf = (char *) pIOBuf;
	long	bufSiz = pBytes->memIOBuf;

	long	nfrom, nto;

	do
	{
		nfrom = read_levelfile(inRefNum, buf, bufSiz);
		if (! in.Recover)
			return;

		nto = write_savefile(outRefNum, buf, nfrom);
		if (! in.Recover)
			return;

		if (nto != nfrom)
		{
			endRecover();
			note(noErr, alidNote, "\pSorry: File Copy Error");
			return;
		}
	}
	while (nfrom == bufSiz);
}

static void
restore_savefile()
{
	static int	savelev;
	long		saveTemp, lev;
	xchar		levc;
	struct version_info version_data;

	/* level 0 file contains:
	 *	pid of creating process (ignored here)
	 *	level number for current level of save file
	 *	name of save file nethack would have created
	 *	and game state
	 */

	lev = in.Recover - 1;
	if (lev == 0L)
	{
		gameRefNum = open_levelfile(0L);

		if (in.Recover)
			(void) read_levelfile(gameRefNum, (Ptr) &hpid, sizeof(hpid));

		if (in.Recover)
			saveTemp = read_levelfile(gameRefNum, (Ptr) &savelev, sizeof(savelev));

		if (in.Recover && (saveTemp != sizeof(savelev)))
		{
			endRecover();
			note(noErr, alidNote, "\pSorry: \"checkpoint\" was not enabled");
			return;
		}

		if (in.Recover)
			(void) read_levelfile(gameRefNum, (Ptr) savename, sizeof(savename));
		if (in.Recover)
			(void) read_levelfile(gameRefNum,
				    (Ptr) &version_data, sizeof version_data);

		/* save file should contain:
		 *	current level (including pets)
		 *	(non-level-based) game state
		 *	other levels
		 */
		if (in.Recover)
			saveRefNum = create_savefile(savename);

		if (in.Recover)
			levRefNum = open_levelfile(savelev);

		if (in.Recover)
			(void) write_savefile(saveRefNum,
				    (Ptr) &version_data, sizeof version_data);
		if (in.Recover)
			copy_bytes(levRefNum, saveRefNum);

		if (in.Recover)
			close_file(&levRefNum);

		if (in.Recover)
			unlink_file(lock);

		if (in.Recover)
			copy_bytes(gameRefNum, saveRefNum);

		if (in.Recover)
			close_file(&gameRefNum);

		if (in.Recover)
			set_levelfile_name(0L);

		if (in.Recover)
			unlink_file(lock);
	}
	else if (lev != savelev)
	{
		levRefNum = open_levelfile(lev);
		if (levRefNum >= 0)
		{
			/* any or all of these may not exist */
			levc = (xchar) lev;

			(void) write_savefile(saveRefNum, (Ptr) &levc, sizeof(levc));

			if (in.Recover)
				copy_bytes(levRefNum, saveRefNum);

			if (in.Recover)
				close_file(&levRefNum);

			if (in.Recover)
				unlink_file(lock);
		}
	}

	if (in.Recover == MAX_RECOVER_COUNT)
		close_file(&saveRefNum);

	if (in.Recover)
		in.Recover++;
}

static long
read_levelfile(short rdRefNum, Ptr bufPtr, long count)
{
	OSErr	rdErr;
	long	rdCount = count;

	if ((rdErr = FSRead(rdRefNum, &rdCount, bufPtr)) && (rdErr != eofErr))
	{
		endRecover();
		note(noErr, alidNote, "\pSorry: File Read Error");
		return (-1L);
	}

	return rdCount;
}

static long
write_savefile(short wrRefNum, Ptr bufPtr, long count)
{
	long	wrCount = count;

	if (FSWrite(wrRefNum, &wrCount, bufPtr))
	{
		endRecover();
		note(noErr, alidNote, "\pSorry: File Write Error");
		return (-1L);
	}

	return wrCount;
}

static void
close_file(short *pFRefNum)
{
	if (FSClose(*pFRefNum) || FlushVol((StringPtr) 0L, vRefNum))
	{
		endRecover();
		note(noErr, alidNote, "\pSorry: File Close Error");
		return;
	}

	*pFRefNum = -1;
}

static void
unlink_file(unsigned char *filename)
{
	if (HDelete(vRefNum, dirID, filename))
	{
		endRecover();
		note(noErr, alidNote, "\pSorry: File Delete Error");
		return;
	}
}

