// PCompress
// Copyright (C) 1999 Britt Yenne
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
// 
// To contact the author, visit:  http://www.jump.net/~syenne/pilot/

#pragma pack(2)
#include <Common.h>
#include <System/SysAll.h>
#include <UI/UIAll.h>

#include "header.h"
#include "pcomp.h"
#include "dbinfo.h"
#include "gui.h"

#ifdef __GNUC__
  register void *reg_a4 asm("%a4");
# define CALLBACK_PROLOGUE \
    void *save_a4 = reg_a4; asm("move.l %%a5,%%a4; sub.l #edata,%%a4" : :);
# define CALLBACK_EPILOGUE reg_a4 = save_a4;
#else
# define CALLBACK_PROLOGUE
# define CALLBACK_EPILOGUE
#endif

// GUI variables:
//
static DBInfo *dbList = 0;		// main database list
static UInt dbCount = 0, dbMax = 0;	// main database list size / max
static UInt dbDeletes = 0;		// available dbList slots
static Boolean dbListFull = false;	// list needs to be refreshed

static DBInfo **dbView = 0;		// current database view
static UInt dbvCount = 0, dbvMax = 0;	// database view list size / max
static DBInfo *selectDB;		// currently selected database

static TablePtr mainTbl;
static int mainTblRows = 0, mainTblTop = 0;
static Boolean testing = false;

// Variables used during compression:
//
static UInt pRef;			// PCompress library refNum
static PCompCtl pCtl = 0;		// current pcomp control instance

static int VectorMax = VectorMaxDflt;	// max vectors in each section
static DBInfo *compDB;			// currently selected database
static DmOpenRef inDB = 0;		// input database
static DmOpenRef outDB = 0;		// output database
static VoidHand outHand = 0, inHand = 0; // current record handles
static VoidPtr outPtr;			// current record pointer
static UInt inRec, outRec;		// current I/O record numbers
static UInt outLen, outMax;		// current record position
static ULong outTotal;			// total bytes output
static Vector *vect = 0;		// record vector
static UInt vCount, vMax;		// vector count, max
static DBInfoHdr dbHdr;			// database info header
static DBRecordHdr *recHdr = 0;		// record header array
static UInt recInfoPos, recInfos;	// record header count and position
static VoidHand appInfoHand;		// application info block handle
static VoidHand sortInfoHand;		// sort info block handle
static Boolean justReadingHeader = false; // used by NewDBInfo to read header

static enum {
  ISRecord = 0,
  ISHeader,
  ISAppInfo,
  ISSortInfo,
  ISRecInfo
} inputState;

// Variables used for the progress meter:
//
static ULong progByteBase, progByteDone, progByteTotal;
static UInt progEncDone, progEncTotal;
static ULong progEncByteDone, progEncByteTotal;

VoidPtr
ReAlloc(VoidPtr ptr, ULong size)
{
  if (ptr == 0 || MemPtrResize(ptr, size) != 0) {
    void *out = MemPtrNew(size);
    if (ptr && out) {
      MemMove(out, ptr, MemPtrSize(ptr));
      MemPtrFree(ptr);
    }
    ptr = out;
  }
  return ptr;
}

VoidPtr
Alloc(ULong size)
{
  return ReAlloc(0, size);
}

void
Free(void *ptr)
{
  MemPtrFree(ptr);
}

static void
SavePrefs()
{
  PrefSetAppPreferences(pcompCreator, 0, 1, &prefs, sizeof(prefs), true);
}

static int
ReadPrefs()
{
  Word size;
  SWord rc;

  size = sizeof(prefs);
  rc = PrefGetAppPreferences(pcompCreator, 0, &prefs, &size, true);
  if (rc == noPreferenceFound || size != sizeof(prefs)) {
    MemSet(&prefs, sizeof(prefs), 0);
    prefs.level = 2;
  }
  SysRandom(TimGetTicks());	// seed random generator
  return 0;
}

static int
StartApplication(void)
{
  int err;

  if ((err = ReadPrefs()) != 0)
    return err;
#ifdef PCOMP_SHLIB
  if ((err = SysLibFind("PCompress Library", &pRef)) != 0)
    if ((err = SysLibLoad('libr', pcompCreator, &pRef)) != 0)
      return err;
  if ((err = PCompLibOpen(pRef)) != 0)
    return err;
#endif
  if ((pCtl = PCompOpen(pRef, Alloc, Free)) == 0)
    return 1;
  FrmGotoForm(formID_main);
}

static void
Clear(int x, int y, int w, int h)
{
  RectangleType rect;

  rect.topLeft.x = x;
  rect.topLeft.y = y;
  rect.extent.x = w;
  rect.extent.y = h;
  WinEraseRectangle(&rect, 0);
}

static void
Print(int x, int y, char *str, FontID font)
{
  FntSetFont(font);
  WinDrawChars(str, StrLen(str), x, y);
}

static int
CheckCancel(void)
{
  EventType event;
  int rc = 0;

  //
  // Check for the user to cancel the operation.  We ignore any events
  // which aren't the cancel button.
  //
  while (EvtEventAvail()) {
    EvtGetEvent(&event, 0);
    if (event.eType == ctlSelectEvent
     && event.data.ctlSelect.controlID == buttonID_cancel)
      rc = rcCancel;
    else
      FrmDispatchEvent(&event);
  }
  return rc;
}

static int
ProgressMeter(char *msg)
{
  ULong meter;
  RectangleType rect;
  int n, x, y, rc;

  //
  // If the compress form is not active, then there is no meter
  // and no cancel button.  Presumably this only occurs when we
  // are reading compressed headers while building the database
  // list.
  //
  if (FrmGetActiveFormID() != formID_dbComp)
    return 0;

  //
  // The progress is measured:
  //    80% by the bytecount completed so far
  //    20% by the database(s) completed so far
  //
  meter = 0;
  if (progByteTotal)
    meter += progByteDone * (progEncTotal ? 80 : 100) / progByteTotal;
  if (progEncTotal) {
    meter += ((ULong)progEncDone) * (progByteTotal ? 20 : 100)
      / progEncTotal;
    if (progEncByteTotal)
      meter += progEncByteDone * (progByteTotal ? 20 : 100)
	/ progEncByteTotal / progEncTotal;
  }
  if (meter > 100)
    meter = 100;

  //
  // Draw the meter.
  //
# define MeterX		15
# define MeterY		50
# define MeterWidth	120
# define MeterHeight	16
  rect.topLeft.x = MeterX;
  rect.topLeft.y = MeterY;
  rect.extent.x = MeterWidth;
  rect.extent.y = MeterHeight;
  WinDrawRectangleFrame(rectangleFrame, &rect);
  rect.extent.x = meter * MeterWidth / 100;
  WinFillRectangle(&rect, 0);

  //
  // Draw the optional message.
  //
  if (msg) {
    FntSetFont(boldFont);
    n = StrLen(msg);
    x = MeterX + (MeterWidth - FntCharsWidth(msg, n)) / 2;
    y = MeterY + (MeterHeight - FntLineHeight()) / 2;
    WinDrawChars(msg, n, x, y);
  }

  return CheckCancel();
}

static void
ProgressStatus(char *action, char *name)
{
  int n;

  //
  // If the compress form is not active, then return immediately.
  // Presumably this only occurs when we are reading compressed headers
  // while building the database list.
  //
  if (FrmGetActiveFormID() != formID_dbComp)
    return;

  //
  // Draw the status message area.
  //
  Clear(1, 5, 148, 30);
  if (action) {
    FntSetFont(boldFont);
    n = StrLen(action);
    WinDrawChars(action, n, (150 - FntCharsWidth(action, n)) / 2, 5);
  }
  if (name) {
    FntSetFont(stdFont);
    n = StrLen(name);
    WinDrawChars(name, n, (150 - FntCharsWidth(name, n)) / 2, 18);
  }
}

static void
ProgressComplete(UInt dbs)
{
  char buf[40];

  StrPrintF(buf, "%d database%s processed", dbs, ((dbs == 1) ? "" : "s"));
  ProgressStatus(0, buf);
}

static int
Progress(PCompCtl ctl, s32bit val)
{
  progByteDone = progByteBase + val;
  return ProgressMeter(0);
}

static s32bit
FlushComp(PCompCtl ctl, u8bit *buf, u16bit len, Vector **vectp)
{
  UInt at;
  u16bit count;
  s32bit rc;

  while (len > 0) {
    if ((count = outMax - outLen) > len)
      count = len;
    if (count > 0) {
      if (outDB == 0)
	MemMove((char*)outPtr + outLen, buf, count);
      else if (DmWrite(outPtr, outLen, buf, count) != 0)
	return rcDmError;
      buf += count;
      len -= count;
      outLen += count;
      outTotal += count;
    }
    if (len > 0) {
      if (outDB == 0)
	return rcNoDmMem;	// overflowed static output buffer
      if (outHand)
	MemHandleUnlock(outHand);
      at = inRec;
      for (outMax = 4096; outMax >= 512; outMax >>= 1)
	if ((outHand = DmNewRecord(outDB, &at, outMax)) != 0)
	  break;
      if (!outHand)
	return rcNoDmMem;
      if (at != inRec)
	return rcFail;
      inRec++;
      outPtr = MemHandleLock(outHand);
      outLen = 0;
    }
  }
  return vectp ? rcFail : outTotal;
}

static s32bit
FlushUncomp(PCompCtl ctl, u8bit *buf, u16bit len, Vector **vectp)
{
  UInt at;
  s32bit rc;
  VoidHand oldHand;
  VoidPtr oldPtr;

  //
  // We check for (outMax == 0) here to make sure that any zero-length
  // database records get created.
  //
  if (len > outMax - outLen || outMax == 0) {
    //
    // Resize the output record to be large enough to hold this
    // chunk of data.
    //
    if (inputState == ISHeader || inputState == ISRecInfo) {
      if (inputState == ISHeader) {
	MemMove((char*)outPtr + outLen, buf, outMax - outLen);
	if (dbHdr.appMagic == AppMagic && dbHdr.appVersion > AppVersion)
	  return rcBadVersion;
      }
      return rcBadData;		// overflowed a static buffer
    }
    if (outHand) {
      MemHandleUnlock(outHand);
      outMax = outLen + len;
      if (inputState != ISRecord) {
	if (MemHandleResize(outHand, outMax) != 0) {
	  oldHand = outHand;
	  if ((outHand = DmNewHandle(outDB, outMax)) != 0) {
	    oldPtr = MemHandleLock(oldHand);
	    outPtr = MemHandleLock(outHand);
	    if (DmWrite(outPtr, 0, oldPtr, outLen) != 0) {
	      MemHandleUnlock(oldHand);
	      MemHandleFree(oldHand);
	      return rcDmError;
	    }
	    MemHandleUnlock(outHand);
	    MemHandleUnlock(oldHand);
	  }
	  MemHandleFree(oldHand);
	}
      }
      else if (compDB->resourceDB)
	outHand = DmResizeResource(outHand, outMax);
      else
	outHand = DmResizeRecord(outDB, outRec, outMax);
    }
    else {
      at = outRec;
      if ((outMax = len) == 0)
	outMax = 1;		// can't allocate zero bytes
      if (inputState != ISRecord)
	outHand = DmNewHandle(outDB, outMax);
      else if (compDB->resourceDB) {
	if (vCount >= vMax || recInfoPos >= recInfos)
	  return rcBadData;	// exceeded maximum vector count
	outHand = DmNewResource(outDB, recHdr[recInfoPos].res.resType,
	  recHdr[recInfoPos].res.resID, outMax);
      }
      else
	outHand = DmNewRecord(outDB, &at, outMax);
      if (at != outRec && outHand)
	return rcFail;
    }
    if (!outHand)
      return rcNoDmMem;
    vect[vCount].ptr = outPtr = MemHandleLock(outHand);
    if (vCount > 0)
      vect[vCount].ptr -= vect[vCount-1].extent;
  }
  if (inputState == ISHeader || inputState == ISRecInfo)
    MemMove((char*)outPtr + outLen, buf, len);
  else if (DmWrite(outPtr, outLen, buf, len) != 0)
    return rcDmError;
  outLen += len;
  outTotal += len;
  if (vectp)
    *vectp = vect;

  return outTotal;
}

static int
NewRecord(PCompCtl ctl)
{
  UInt recAttr;

  if (!justReadingHeader) {
    if (vCount >= vMax)
      return rcFail;
    vect[vCount].ptr = outPtr;
    vect[vCount].extent = outLen;
    vect[vCount].handle = (void*)outHand;
    if (vCount > 0) {
      vect[vCount].ptr -= vect[vCount-1].extent;
      vect[vCount].extent += vect[vCount-1].extent;
    }
    vCount++;
  }

  if (inputState == ISRecord && outHand) {
    outMax = 0;
    outHand = 0;
    if (!compDB->resourceDB) {
      if (recInfoPos >= recInfos)
	return rcBadData;	// no record header for this record
      recAttr = recHdr[recInfoPos].rec.attr;
      if (recAttr & dmRecAttrNoData) {
	//
	// This is a deleted record -- delete the bogus zero-length
	// record we created.
	//
	if (outLen != 0)
	  return rcBadData;	// deleted non-zero-length record???
	MemHandleUnlock((VoidHand)vect[vCount-1].handle);
	vect[vCount-1].ptr = 0;
	vect[vCount-1].handle = 0;
	if (DmDeleteRecord(outDB, outRec) < 0)
	  return rcDmError;
	recAttr &= ~(dmRecAttrNoData);
      }
      if (DmSetRecordInfo(outDB, outRec, &recAttr,
	&recHdr[recInfoPos].rec.uniqueID) != 0)
	  return rcDmError;
    }
    recInfoPos++;
    outRec++;
  }
  else {
    switch (inputState) {
      case ISHeader:
	if (outLen < sizeof(dbHdr.appMagic) + sizeof(dbHdr.appVersion)
	 || dbHdr.appMagic != AppMagic)
	  return rcBadData;	// not a valid DB info header
	switch (dbHdr.appVersion) {
	  case 0:		// initial release
	    if (outLen != sizeof(DBInfoHdr))
	      return rcBadData;
	    break;
	  default:
	    return rcBadVersion;	// unsupported PCompress version
	}
	if (justReadingHeader)
	  return rcEndOfHeader;
	if (dbHdr.hasAppInfo || dbHdr.hasSortInfo) {
	  inputState = dbHdr.hasAppInfo ? ISAppInfo : ISSortInfo;
	  outPtr = 0;
	  outMax = 0;
	}
	else {
	  inputState = ISRecInfo;
	  outPtr = (u8bit*)recHdr;
	  outMax = vMax * sizeof(DBRecordHdr);
	}
	break;

      case ISAppInfo:
	appInfoHand = outHand;
	outHand = 0;
	if (dbHdr.hasSortInfo) {
	  inputState = ISSortInfo;
	  outPtr = 0;
	  outMax = 0;
	}
	else {
	  inputState = ISRecInfo;
	  outPtr = (u8bit*)recHdr;
	  outMax = vMax * sizeof(DBRecordHdr);
	}
	break;

      case ISSortInfo:
	sortInfoHand = outHand;
	outHand = 0;
	inputState = ISRecInfo;
	outPtr = (u8bit*)recHdr;
	outMax = vMax * sizeof(DBRecordHdr);
	break;

      case ISRecInfo:
	recInfos = outLen / sizeof(DBRecordHdr);
	if (outLen != recInfos * sizeof(DBRecordHdr))
	  return rcBadData;
	recInfoPos = 0;
	inputState = ISRecord;
	outMax = 0;
	break;
    }
  }
  outLen = 0;

  return 0;
}

static int
NewSection(PCompCtl ctl)
{
  int i, rc;

  //
  // End the records and clear the vector.
  //
  if ((rc = NewRecord(ctl)) < 0)
    return rc;
  for (i = 0; i < vCount; i++)
    if (vect[i].handle)
      MemHandleUnlock((VoidHand)vect[i].handle);
  vCount = 0;

  //
  // Set up for a new recInfo list.
  //
  if (recInfoPos != recInfos || inputState != ISRecord)
    return rcBadData;
  inputState = ISRecInfo;
  outPtr = (u8bit*)recHdr;
  outMax = vMax * sizeof(DBRecordHdr);

  return 0;
}

static s32bit
GetChunk(PCompCtl ctl, u8bit **bufPtr)
{
  Err err;
  int rc;

  if (inHand) {
    MemHandleUnlock(inHand);
    if (!justReadingHeader)
      progEncByteDone += MemHandleSize(inHand);
    inHand = 0;
    if (inDB != outDB)
      inRec++;
    else if ((err = DmRemoveRecord(outDB, inRec)) < 0)
      return rcDmError;
    if (!justReadingHeader && (rc = ProgressMeter(0)) < 0)
      return rc;
  }
  if ((inHand = DmQueryRecord(inDB, inRec)) == 0)
    return (inRec >= DmNumRecords(inDB)) ? 0 : rcDmLock;
  *bufPtr = (u8bit*)MemHandleLock(inHand);
  return MemHandleSize(inHand);
}

static void
ResetDBList(void)
{
  if (dbList) {
    Free(dbList), dbList = 0;
    dbCount = dbMax = dbDeletes = 0;
  }
  if (dbView) {
    Free(dbView), dbView = 0;
    dbvCount = dbvMax = 0;
  }
}

static DBInfo *
NewDBInfo(UInt card, LocalID id, int resize)
{
  int i;
  DmOpenRef ref;
  void *ptr;

  if (dbDeletes == 0)
    i = dbCount;
  else {
    dbDeletes--;
    for (i = 0; i < dbCount; i++)
      if (!dbList[i].valid)
	break;
    if (i >= dbCount)
      dbDeletes = 0;
  }
  if (i >= dbMax) {
    if (resize <= 0) {
      dbListFull = true;
      return 0;
    }
    dbMax += resize;
    if ((ptr = ReAlloc(dbList, dbMax * sizeof(DBInfo))) == 0) {
      FrmAlert(alertID_noMem);
      dbMax = dbCount;
      return 0;
    }
    dbList = (DBInfo*)ptr;
  }
  dbList[i].card = card;
  dbList[i].id = id;
  if (DmDatabaseInfo(card, id, dbList[i].name, &dbList[i].flags,
    0, 0, 0, 0, 0, 0, 0,
    &dbList[i].type, &dbList[i].creator) != 0)
      return 0;
  DmDatabaseSize(card, id, 0, &dbList[i].size, 0);
  dbList[i].origSize = 0;
  dbList[i].resourceDB =
    (dbList[i].flags & dmHdrAttrResDB) ? true : false;
  dbList[i].compressed = false;
  if (dbList[i].creator == pcompCompressed) {
    if ((inDB = DmOpenDatabase(card, id, dmModeReadOnly)) != 0) {
      //
      // Fill in the pcompress control structure.
      //
      PCompSet(pRef, pCtl, pCompFlush, FlushUncomp);
      PCompSet(pRef, pCtl, pCompGetChunk, GetChunk);
      PCompSet(pRef, pCtl, pCompNewRecord, NewRecord);
      PCompSet(pRef, pCtl, pCompNewSection, NewSection);

      //
      // Decompress the database info header.
      //
      inRec = 0;
      outPtr = &dbHdr;
      outLen = outTotal = 0;
      outMax = sizeof(dbHdr);
      inputState = ISHeader;
      justReadingHeader = true;		// Major hack, eh?
      if (PCompDecode(pRef, pCtl) == rcEndOfHeader) {
	MemMove(dbList[i].name, dbHdr.name, sizeof(dbHdr.name));
	dbList[i].type = dbHdr.type;
	dbList[i].creator = dbHdr.creator;
	dbList[i].origSize = dbHdr.size;
	dbList[i].resourceDB =
	  (dbHdr.flags & dmHdrAttrResDB) ? true : false;
	dbList[i].compressed = true;
      }
      justReadingHeader = false;
      if (inHand)
	MemHandleUnlock(inHand), inHand = 0;
      DmCloseDatabase(inDB), inDB = 0;
    }
  }
  dbList[i].name[32] = '\0';	// just in case
  dbList[i].valid = true;
  if (i >= dbCount)
    dbCount = i + 1;
  return &dbList[i];
}

static void
LoadDBInfo(void)
{
  Boolean new = true;
  DmSearchStateType state;
  UInt card;
  LocalID id;

  //
  // Scan all databases.
  //
  dbListFull = false;
  dbCount = dbvCount = dbDeletes = 0;
  while (DmGetNextDatabaseByTypeCreator(new, &state, (ULong)0, (ULong)0,
    (Boolean)false, &card, &id) == 0) {
      new = false;
      if (MemLocalIDKind(id) == memIDPtr)	// ROM database
	continue;
      if (NewDBInfo(card, id, 25) == 0)
	break;
    }
}

static Int
CmpDBInfo(DBInfo **db1, DBInfo **db2, int other)
{
  return StrCompare((*db1)->name, (*db2)->name);
}

static void
SetDBView()
{
  UInt i;
  void *ptr;

  dbvCount = 0;
  for (i = 0; i < dbCount; i++) {
    if (!dbList[i].valid)
      continue;
    if ((prefs.viewApps && dbList[i].type != 'appl')
     || (prefs.view == DBViewUncomp && dbList[i].compressed)
     || (prefs.view == DBViewComp && !dbList[i].compressed))
      continue;
    if (dbvCount >= dbvMax) {
      dbvMax += 25;
      if ((ptr = ReAlloc(dbView, dbvMax * sizeof(DBInfo*))) == 0) {
	FrmAlert(alertID_noMem);
	dbvMax = dbvCount;
	break;
      }
      dbView = (DBInfo**)ptr;
    }
    dbView[dbvCount++] = &dbList[i];
  }
  SysInsertionSort(dbView, dbvCount, sizeof(DBInfo*),
    (CmpFuncPtr)CmpDBInfo, 0);
}

static Boolean
DeleteDB(DBInfo *db)
{
  if (!db->valid)
    return false;
  if (DmDeleteDatabase(db->card, db->id) != 0) {
    FrmCustomAlert(alertID_cantDelete, db->name, 0, 0);
    return false;
  }
  MemSet(db, sizeof(DBInfo), 0);
  dbDeletes++;
  return true;
}

static Boolean
DeleteDBbyID(UInt card, LocalID id)
{
  int i;
  Boolean rc;
  char name[34];

  for (i = 0; i < dbCount; i++)
    if (dbList[i].valid && dbList[i].card == card && dbList[i].id == id)
      break;
  if (i < dbCount)
    rc = DeleteDB(&dbList[i]);
  else if (!(rc = (DmDeleteDatabase(card, id) == 0))) {
    if (DmDatabaseInfo(card, id, name, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0) == 0) {
	name[32] = '\0';
	FrmCustomAlert(alertID_cantDelete, name, 0, 0);
      }
  }
  return rc;
}

static int
CheckCompExists(char *name)
{
  int i;

  //
  // Check for existing instances of compressed data for this database
  // and ask the user if he wants to delete them.
  //
  for (i = 0; i < dbCount; i++)
    if (dbList[i].valid
     && dbList[i].compressed
     && StrCompare(dbList[i].name, name) == 0) {
      switch (FrmCustomAlert(alertID_compExists, name, 0, 0)) {
	case 0:	// skip
	  return rcSkip;

	case 1:	// delete
	  if (!DeleteDB(&dbList[i]))
	    return rcDmError;
	  break;

	case 2:	// ignore
	  return 0;

	default:
	  return rcCancel;
      }
    }
  return 0;
}

static int
CheckUncompExists(char *name)
{
  LocalID id;

  //
  // Check for an existing database and prompt the user whether he
  // wants to delete it or skip it.
  //
  if ((id = DmFindDatabase(0, name)) != 0) {
    switch (FrmCustomAlert(alertID_uncompExists, name, 0, 0)) {
      case 0:	// skip
	return rcSkip;

      case 1:	// delete
	if (!DeleteDBbyID(0, id))
	  return rcDmError;
	return 0;

      default:
	return rcCancel;
    }
  }
  return 0;
}

static int
Compress(DBInfo *db, UInt *pCount)
{
  DmOpenRef ref = 0;
  UInt recs, i, rec, hdrRec, dbs = 0, dbindex;
  ULong size, total, creator;
  VoidHand hand;
  char outName[32];
  LocalID id, outID = 0, appID, sortID, chunkID;
  VoidPtr appPtr = 0, sortPtr = 0;
  Err err;
  Long rc;
  DBInfoHdr hdr;

  //
  // Clear globals.
  //
  if (pCount)
    *pCount = 0;
  outHand = inHand = 0;
  outDB = 0;
  vect = 0;
  recHdr = 0;

  //
  // Determine how much we're compressing and initialize the progress
  // meter.
  //
  creator = db->creator;
  progByteBase = 0;
  progByteDone = 0;
  progByteTotal = db->size;
  progEncDone = 0;
  progEncTotal = 1;
  progEncByteDone = 0;
  progEncByteTotal = 0;
  if (db->type == 'appl' && prefs.compData) {
    for (i = 0; i < dbCount; i++)
      if (dbList[i].valid
        && dbList[i].type != 'appl'
	&& dbList[i].creator == creator
	&& dbList[i].compressed == false) {
	  progByteTotal += dbList[i].size;
	  progEncTotal++;
	}
  }
  if ((rc = ProgressMeter(0)) < 0)
    goto cleanup;

  //
  // Process each database.
  //
  dbindex = 0;
  for (dbs = 0; dbs < progEncTotal; dbs++) {
    //
    // Find the next database to process.
    //
    if (dbs > 0) {
      for (; dbindex < dbCount; dbindex++)
	if (dbList[dbindex].valid
	  && dbList[dbindex].type != 'appl'
	  && dbList[dbindex].creator == creator
	  && dbList[dbindex].compressed == false) {
	    db = &dbList[dbindex];
	    break;
	  }
      if (dbindex++ >= dbCount)
	break;
    }

    //
    // Update the progess window with the database we're processing.
    //
    ProgressStatus("Compressing", db->name);

    //
    // Find existing compressed data for this database
    //
    if ((rc = CheckCompExists(db->name)) < 0) {
      if (rc == rcSkip) {
	progByteBase += db->size;
	progByteDone = progByteBase;
	progEncDone++;
	if ((rc = ProgressMeter(0)) >= 0)
	  continue;
      }
      goto cleanup;
    }

    //
    // Fill in the pcompress control structure.  We do this each
    // time because some other function (like NewDBInfo()) might
    // have changed things.
    //
    PCompSet(pRef, pCtl, pCompLevel, (void*)((s32bit)prefs.level));
    PCompSet(pRef, pCtl, pCompProgress, Progress);
    PCompSet(pRef, pCtl, pCompFlush, FlushComp);
    PCompSet(pRef, pCtl, pCompGetChunk, GetChunk);

    //
    // Initialize the database compression header with information about
    // this database.
    //
    compDB = db;
    MemSet(&hdr, sizeof(hdr), 0);
    hdr.appMagic = AppMagic;
    hdr.appVersion = AppVersion;
    if (DmDatabaseInfo(db->card, db->id, hdr.name, &hdr.flags,
      &hdr.version, &hdr.crDate, &hdr.modDate, &hdr.bckUpDate, &hdr.modNum,
      &appID, &sortID, &hdr.type, &hdr.creator) != 0) {
	rc = rcDmError;
	goto cleanup;
      }
    hdr.size = db->size;

    //
    // Open the input database.
    //
    if ((ref = DmOpenDatabase(db->card, db->id, dmModeReadOnly)) == 0) {
      rc = rcDmLock;
      goto cleanup;
    }
    if ((recs = DmNumRecords(ref)) == 0) {
      DmCloseDatabase(ref), ref = 0;
      progByteBase += db->size;
      progByteDone = progByteBase;
      progEncDone++;
      FrmCustomAlert(alertID_cantCompress, db->name, 0, 0);
      continue;
    }

    //
    // Open the output database
    // 
    do {
      StrPrintF(outName, "Compressed Data %ld",
	((((ULong)SysRandom(0)) << 16) | TimGetTicks()) % (ULong)1000000000);
    } while (DmFindDatabase(0, outName) != 0);
    if ((err = DmCreateDatabase(0, outName, pcompCompressed,
      'comp', false)) != 0
     || (outID = DmFindDatabase(0, outName)) == 0
     || (outDB = DmOpenDatabase(0, outID, dmModeReadWrite)) == 0) {
      rc = rcDmError;
      goto cleanup;
    }
    outHand = inHand = 0;
    outMax = outLen = 0;
    outTotal = 0;
    inRec = 0;

    //
    // Allocate needed memory.
    //
    vMax = VectorMax;	// always, for simplicity -- trust me
    if ((vect = (Vector*)Alloc(vMax * sizeof(Vector))) == 0
     || (recHdr = (DBRecordHdr*)Alloc(vMax * sizeof(DBRecordHdr))) == 0) {
      rc = rcNoMem;
      goto cleanup;
    }
    vCount = 0;
    total = 0;

    //
    // Place the database information header.
    //
    vect[vCount].ptr = ((u8bit*)&hdr) - total;
    vect[vCount].extent = (size = sizeof(hdr)) + total;
    vect[vCount].handle = 0;
    total += size;
    vCount++;

    //
    // Place the application info block if it exists.
    //
    if (appID) {
      if ((appPtr = MemLocalIDToLockedPtr(appID, db->card)) == 0) {
	rc = rcDmError;
	goto cleanup;
      }
      hdr.hasAppInfo = 1;
      vect[vCount].ptr = ((u8bit*)appPtr) - total;
      vect[vCount].extent = (size = MemPtrSize(appPtr)) + total;
      vect[vCount].handle = 0;
      total += size;
      vCount++;
      progByteTotal += size;
    }

    //
    // Place the sort info block if it exists.
    //
    if (sortID) {
      if ((sortPtr = MemLocalIDToLockedPtr(sortID, db->card)) == 0) {
	rc = rcDmError;
	goto cleanup;
      }
      hdr.hasSortInfo = 1;
      vect[vCount].ptr = ((u8bit*)sortPtr) - total;
      vect[vCount].extent = (size = MemPtrSize(sortPtr)) + total;
      vect[vCount].handle = 0;
      total += size;
      vCount++;
      progByteTotal += size;
    }

    //
    // Build the vector array with a pointer to each record, and perform
    // phase one of the compression.
    //
    rc = 0;
    rec = 0;
    hdrRec = 0;
    while (rec < recs && rc >= 0) {
      //
      // Write out a new record info array.
      //
      for (i = 0; i < vMax-vCount-1 && hdrRec < recs; i++, hdrRec++) {
	if (db->resourceDB)
	  err = DmResourceInfo(ref, hdrRec, &recHdr[i].res.resType,
	    &recHdr[i].res.resID, 0);
	else {
	  err = DmRecordInfo(ref, hdrRec, &recHdr[i].rec.attr,
	    &recHdr[i].rec.uniqueID, &chunkID);
	  if (err == 0) {
	    if (recHdr[i].rec.attr & dmRecAttrNoData) {
	      //
	      // This shouldn't be a valid flag.
	      //
	      rc = rcBadData;
	      break;
	    }
	    if ((recHdr[i].rec.attr & dmRecAttrDelete) && !chunkID)
	      recHdr[i].rec.attr |= dmRecAttrNoData;
	  }
	}
	if (err) {
	  rc = rcDmError;
	  break;
	}
      }
      vect[vCount].ptr = ((u8bit*)recHdr) - total;
      vect[vCount].extent = (size = i * sizeof(DBRecordHdr)) + total;
      vect[vCount].handle = 0;
      total += size;
      vCount++;

      //
      // Write out record pointers.
      //
      if (rc >= 0) {
	for (i = 0; rec < hdrRec; rec++, i++) {
	  if (db->resourceDB)
	    hand = DmGetResourceIndex(ref, rec);
	  else {
	    //
	    // We can't call DmQueryRecord here because it will fail
	    // for archived records, whereas we want to save the
	    // archived data.
	    //
	    if (recHdr[i].rec.attr & dmRecAttrNoData) {
	      //
	      // No data associated with this record.  We need to write
	      // out a fake zero-length vector so that we at least get
	      // a call to NewRecord().
	      //
	      vect[vCount].ptr = 0;
	      vect[vCount].extent = total;
	      vect[vCount].handle = 0;
	      vCount++;
	      continue;
	    }
	    if (DmRecordInfo(ref, rec, 0, 0, &chunkID) != 0)
	      hand = 0;
	    else
	      hand = (VoidHand)MemLocalIDToGlobal(chunkID, db->card);
	  }
	  if (hand == 0) {
	    rc = rcDmLock;
	    break;
	  }
	  vect[vCount].ptr = ((u8bit*)MemHandleLock(hand)) - total;
	  vect[vCount].extent = (size = MemHandleSize(hand)) + total;
	  vect[vCount].handle = (void*)hand;
	  total += size;
	  vCount++;
	}
      }

      //
      // Perform the deflate.
      //
      if (rc >= 0)
	rc = PCompDeflate(pRef, pCtl, vect, vCount, (rec == recs));
      for (i = 0; i < vCount; i++)
	if (vect[i].handle)
	  MemHandleUnlock((VoidHand)vect[i].handle);
      if (appPtr)
	MemPtrUnlock(appPtr), appPtr = 0;
      if (sortPtr)
	MemPtrUnlock(sortPtr), sortPtr = 0;
      vCount = 0;
      if (rc >= 0) {
	progByteBase += total;
	progByteDone = progByteBase;
	rc = ProgressMeter(0);
      }
      total = 0;
    }
    DmCloseDatabase(ref), ref = 0;
    Free(vect), vect = 0;
    Free(recHdr), recHdr = 0;

    //
    // Flush the output database.
    //
    if (outHand) {
      MemHandleUnlock(outHand);
      if (outLen < outMax)
	MemHandleResize(outHand, outLen);
      outHand = 0;
    }
    if (rc < 0)	// Deflate failed
      goto cleanup;

    //
    // Perform phase two of the compression.
    //
    inDB = outDB;
    outMax = outLen = 0;
    inRec = 0;
    progEncByteTotal = outTotal;
    outTotal = 0;
    if ((rc = PCompBuildTree(pRef, pCtl)) < 0)
      goto cleanup;
    rc = PCompEncode(pRef, pCtl);

    //
    // Flush the output database.
    //
    if (outHand) {
      MemHandleUnlock(outHand);
      if (outLen < outMax)
	MemHandleResize(outHand, outLen);
      outHand = 0;
    }
    DmCloseDatabase(outDB), outDB = 0;
    if (rc < 0)	// Encode failed
      goto cleanup;

    //
    // If the database is larger than the original, display a nice
    // dialog and then delete the "compressed" version.  Oh well,
    // you can't win them all.
    //
    if (!testing
     && DmDatabaseSize(0, outID, 0, &size, 0) == 0
     && size >= db->size) {
      FrmCustomAlert(alertID_cantCompress, db->name, 0, 0);
      DmDeleteDatabase(0, outID);
      outID = 0;
    }
    else {
      //
      // Delete the database if requested, and update database list to
      // include this new DB.  We don't let NewDBInfo() resize its array
      // because memory is tight during compression and we don't want to
      // deal with fragmentation.  If it can't fit the new entry, it'll
      // free the array and rebuild it after the compression is finished.
      //
      // We combine these steps because WE MUST NOT GOTO CLEANUP BETWEEN
      // THE DeleteDB() AND SETTING outID TO ZERO.
      //
      if (prefs.compDelete)
	DeleteDB(db);
      NewDBInfo(0, outID, 0);
      outID = 0;
      if (pCount)
	(*pCount)++;
    }

    //
    // Update progress information.
    //
    progEncDone++;
    progEncByteDone = progEncByteTotal = 0;
    if ((rc = ProgressMeter(0)) < 0)
      goto cleanup;
  }

  //
  // Update the progess window with the number of databases processed.
  //
  rc = 0;

cleanup:
  //
  // Free memory and close databases.
  //
  inDB = 0;
  if (outHand)
    MemHandleUnlock(outHand), outHand = 0;
  if (inHand)
    MemHandleUnlock(inHand), inHand = 0;
  if (appPtr)
    MemPtrUnlock(appPtr), appPtr = 0;
  if (sortPtr)
    MemPtrUnlock(sortPtr), sortPtr = 0;
  if (ref)
    DmCloseDatabase(ref);
  if (outDB)
    DmCloseDatabase(outDB), outDB = 0;
  if (rc < 0 && outID)
    DmDeleteDatabase(0, outID);
  if (vect)
    Free(vect), vect = 0;
  if (recHdr)
    Free(recHdr), recHdr = 0;
  return rc;
}

static int
Uncompress(DBInfo *db, UInt *pCount)
{
  int i;
  s32bit rc;
  LocalID id, outID = 0, appID, sortID, *appIDP, *sortIDP;
  Err err;
  UInt newFlags, dbs = 0, dbindex;
  ULong creator;
  char outName[34];

  //
  // Clear globals.
  //
  if (pCount)
    *pCount = 0;
  outHand = inHand = appInfoHand = sortInfoHand = 0;
  inDB = outDB = 0;
  vect = 0;
  recHdr = 0;

  //
  // Determine how much we're uncompressing and initialize the progress
  // meter.
  //
  creator = db->creator;
  progByteTotal = 0;
  progEncByteDone = 0;
  progEncDone = 0;
  progEncTotal = 1;
  if (db->type == 'appl' && prefs.uncompData) {
    for (i = 0; i < dbCount; i++)
      if (dbList[i].valid
	&& dbList[i].type != 'appl'
	&& dbList[i].creator == creator
	&& dbList[i].compressed == true) {
	  progEncTotal++;
	}
  }
  if ((rc = ProgressMeter(0)) < 0)
    goto cleanup;

  //
  // Process each database.
  //
  dbindex = 0;
  for (dbs = 0; dbs < progEncTotal; dbs++) {
    //
    // Find the next database to process.
    //
    if (dbs > 0) {
      for (; dbindex < dbCount; dbindex++)
	if (dbList[dbindex].valid
	  && dbList[dbindex].type != 'appl'
	  && dbList[dbindex].creator == creator
	  && dbList[dbindex].compressed == true) {
	    db = &dbList[dbindex];
	    break;
	  }
      if (dbindex++ >= dbCount)
	break;
    }

    //
    // Update the progess window with the database we're processing.
    //
    ProgressStatus("Uncompressing", db->name);

    //
    // Find existing uncompressed data for this database
    //
    StrCopy(outName, db->name);
    if ((rc = CheckUncompExists(outName)) < 0) {
      if (rc == rcSkip) {
	progEncDone++;
	if ((rc = ProgressMeter(0)) >= 0)
	  continue;
      }
      goto cleanup;
    }

    //
    // Open the input database.
    //
    compDB = db;
    if ((inDB = DmOpenDatabase(db->card, db->id, dmModeReadOnly)) == 0) {
      rc = rcDmLock;
      goto cleanup;
    }

    //
    // Open the output database.  Don't set the type just yet, since
    // we don't know if the operation will succeed and we don't want
    // garbage data out there with application or system types.
    // 
    if ((err = DmCreateDatabase(0, outName, db->creator,
      'temp', db->resourceDB)) != 0
     || (outID = DmFindDatabase(0, outName)) == 0
     || (outDB = DmOpenDatabase(0, outID, dmModeReadWrite)) == 0) {
      rc = rcDmError;
      goto cleanup;
    }
    outHand = inHand = 0;
    outMax = outLen = 0;
    outTotal = 0;
    inRec = outRec = 0;

    //
    // Fill in the pcompress control structure.  We do this each
    // time because some other function (like NewDBInfo()) might
    // have changed things.
    //
    PCompSet(pRef, pCtl, pCompFlush, FlushUncomp);
    PCompSet(pRef, pCtl, pCompGetChunk, GetChunk);
    PCompSet(pRef, pCtl, pCompNewRecord, NewRecord);
    PCompSet(pRef, pCtl, pCompNewSection, NewSection);

    //
    // Allocate needed memory.
    //
    vMax = VectorMax;
    if ((vect = (Vector*)Alloc(vMax * sizeof(Vector))) == 0
     || (recHdr = (DBRecordHdr*)Alloc(vMax * sizeof(DBRecordHdr))) == 0) {
      rc = rcNoMem;
      goto cleanup;
    }
    vCount = 0;

    //
    // Decompress and flush the output database.
    //
    outPtr = &dbHdr;
    outMax = sizeof(dbHdr);
    inputState = ISHeader;
    recInfos = recInfoPos = 0;
    progEncByteTotal = db->size;
    if ((rc = PCompDecode(pRef, pCtl)) >= 0)
      rc = NewRecord(pCtl);
    if (rc >= 0 && recInfoPos != recInfos)
      rc = rcBadData;
    for (i = 0; i < vCount; i++)
      if (vect[i].handle)
	MemHandleUnlock((VoidHand)vect[i].handle);
    Free(vect), vect = 0;
    Free(recHdr), recHdr = 0;
    DmCloseDatabase(inDB), inDB = 0;
    if (outHand) {
      MemHandleUnlock(outHand);
      if (inputState != ISRecord)
	MemHandleFree(outHand);
      outHand = 0;
    }
    DmCloseDatabase(outDB), outDB = 0;
    if (rc < 0)	// Decode failed
      goto cleanup;

    //
    // Restore database header information.
    //
    if (DmDatabaseInfo(0, outID, 0, &newFlags,
      0, 0, 0, 0, 0, 0, 0, 0, 0) != 0) {
	rc = rcDmError;
	goto cleanup;
      }
    newFlags |= (dbHdr.flags & (dmHdrAttrReadOnly | dmHdrAttrBackup |
      dmHdrAttrOKToInstallNewer | dmHdrAttrResetAfterInstall));
    if (!appInfoHand)
      appIDP = 0;
    else {
      appID = MemHandleToLocalID(appInfoHand);
      appIDP = &appID;
    }
    if (!sortInfoHand)
      sortIDP = 0;
    else {
      sortID = MemHandleToLocalID(sortInfoHand);
      sortIDP = &sortID;
    }
    if (DmSetDatabaseInfo(0, outID, 0, &newFlags,
      &dbHdr.version, &dbHdr.crDate, &dbHdr.modDate,
      &dbHdr.bckUpDate, &dbHdr.modNum,
      appIDP, sortIDP, &dbHdr.type, &dbHdr.creator) != 0) {
	rc = rcDmError;
	goto cleanup;
      }
    appInfoHand = sortInfoHand = 0;

    //XXX handle reset after install bit?

    //
    // Delete the database if requested, and update database list to
    // include this new DB.  We don't let NewDBInfo() resize its array
    // because memory is tight during compression and we don't want to
    // deal with fragmentation.  If it can't fit the new entry, it'll
    // free the array and rebuild it after the compression is finished.
    //
    // We combine these steps because WE MUST NOT GOTO CLEANUP BETWEEN
    // THE DeleteDB() AND SETTING outID TO ZERO.
    //
    if (prefs.uncompDelete)
      DeleteDB(db);
    NewDBInfo(0, outID, 0);
    outID = 0;
    if (pCount)
      (*pCount)++;

    //
    // Update progress information.
    //
    progEncByteDone = 0;
    progEncDone++;
    if ((rc = ProgressMeter(0)) < 0)
      goto cleanup;
  }

  //
  // Update the progess window with the number of databases processed.
  //
  rc = 0;

cleanup:
  //
  // Free memory and close databases.
  //
  if (outHand) {
    MemHandleUnlock(outHand);
    if (inputState != ISRecord)
      MemHandleFree(outHand);
    outHand = 0;
  }
  if (inHand) {
    MemHandleUnlock(inHand);
    inHand = 0;
  }
  if (appInfoHand)
    MemHandleFree(appInfoHand), appInfoHand = 0;
  if (sortInfoHand)
    MemHandleFree(sortInfoHand), sortInfoHand = 0;
  if (inDB)
    DmCloseDatabase(inDB), inDB = 0;
  if (outDB)
    DmCloseDatabase(outDB), outDB = 0;
  if (rc < 0 && outID)
    DmDeleteDatabase(0, outID);
  if (vect)
    Free(vect), vect = 0;
  if (recHdr)
    Free(recHdr), recHdr = 0;
  return rc;
}

static Boolean
SetBackup(DBInfo *db, Boolean backup)
{
  Boolean ok = false;
  UInt newFlags;

  if (DmDatabaseInfo(db->card, db->id, 0, &db->flags,
    0, 0, 0, 0, 0, 0, 0, 0, 0) == 0) {
      if (backup)
	newFlags = (db->flags | dmHdrAttrBackup);
      else
	newFlags = (db->flags & ~((UInt)dmHdrAttrBackup));
      if (DmSetDatabaseInfo(db->card, db->id, 0, &newFlags, 0, 0, 0,
	0, 0, 0, 0, 0, 0) == 0) {
	  db->flags = newFlags;
	  ok = true;
	}
    }
  return ok;
}

static Boolean
HandleMenu(int id)
{
  int rc;
  Boolean handled = false;

  switch (id)
  {
    case menuitemID_about:
      if (FrmAlert(alertID_about) == 1)
	FrmHelp(helpStr_license);
      handled = true;
      break;

    case menuitemID_test:
      FrmPopupForm(formID_test);
      handled = true;
      break;
  }
  return handled;
}

static void
TblDrawString(VoidPtr tbl, Word row, Word col, RectanglePtr bounds)
{
  RectangleType oldClip;
  DBInfo *db;
  char buf[32];
  int n, x;

  if ((db = (DBInfo*)TblGetRowData((TablePtr)tbl, row)) != 0) {
    WinGetClip(&oldClip);
    WinSetClip(bounds);
    switch (col) {
      case colDBName:
	FntSetFont(db->compressed ? boldFont : stdFont);
	WinDrawChars(db->name, StrLen(db->name),
	  bounds->topLeft.x, bounds->topLeft.y);
	break;
      case colDBSize:
	FntSetFont(stdFont);
	n = StrLen(StrIToA(buf, db->size));
	x = bounds->topLeft.x + bounds->extent.x - FntCharsWidth(buf, n) - 2;
	WinDrawChars(buf, n, x, bounds->topLeft.y);
	break;
    }
    WinSetClip(&oldClip);
  }
}

static void
UpdateDBTable(TablePtr tbl, int top, Boolean force)
{
  FormPtr form;
  int i;

  if (top + mainTblRows > dbvCount)
    top = dbvCount - mainTblRows;
  if (top < 0)
    top = 0;
  if (force || top != mainTblTop) {
    TblUnhighlightSelection(tbl);
    for (i = 0; i < mainTblRows && i < dbvCount; i++) {
      TblSetRowUsable(tbl, i, true);
      TblSetRowData(tbl, i, (ULong)dbView[top+i]);
      TblSetItemInt(tbl, i, 0, (dbView[top+i]->flags & dmHdrAttrBackup) ? 1 : 0);
    }
    for (; i < mainTblRows; i++) {
      TblSetRowUsable(tbl, i, false);
      TblSetRowData(tbl, i, 0);
    }
    mainTblTop = top;
    TblMarkTableInvalid(tbl);
    TblRedrawTable(tbl);

    form = FrmGetActiveForm();
    FrmUpdateScrollers(form,
      FrmGetObjectIndex(form, buttonID_up),
      FrmGetObjectIndex(form, buttonID_down),
      (top > 0) ? true : false,
      (top + mainTblRows < dbvCount) ? true : false);
  }
}

static Word
SetSelector(FormPtr form, Word listId, Word triggerId, Word item)
{
  int index;
  ListPtr list;
  ControlPtr ctl;
  char *text;

  list = (ListPtr)FrmGetObjectPtr(form,
    FrmGetObjectIndex(form, listId));
  index = FrmGetObjectIndex(form, triggerId);
  ctl = (ControlPtr)FrmGetObjectPtr(form, index);
  CtlEraseControl(ctl);
  LstSetSelection(list, item);
  text = LstGetSelectionText(list, item);
  FntSetFont(stdFont);
  FrmSetObjectPosition(form, index,
    160 - FntCharsWidth(text, StrLen(text)) - 17, 0);
  CtlSetLabel(ctl, text);
  CtlDrawControl(ctl);
  return item;
}

static void
MakeHelpButton(FormPtr form)
{
  Word index;
  RectangleType rect;

  index = FrmGetObjectIndex(form, buttonID_help);
  FrmGetObjectBounds(form, index, &rect);
  WinEraseRectangle(&rect, 3);
  FrmShowObject(form, index);
}

static Boolean
MainForm(EventPtr event)
{
  FormPtr   form;
  Boolean   handled = false;
  int       id, i, n, rowHeight;
  RectangleType rect;
  DBInfo    *db;

  switch (event->eType)
  {
    case frmOpenEvent:
    case frmUpdateEvent:
      form = FrmGetActiveForm();

      mainTbl = (TablePtr)FrmGetObjectPtr(form,
	FrmGetObjectIndex(form, tableID_main));
      mainTblRows = TblGetNumberOfRows(mainTbl);

      SetSelector(form, listID_view, triggerID_view, prefs.view);
      CtlSetValue((ControlPtr)FrmGetObjectPtr(form,
	FrmGetObjectIndex(form, boxID_apps)), prefs.viewApps);
      if (dbCount <= 0 || dbListFull) {
	for (i = 0; i < mainTblRows; i++)
	  TblSetRowUsable(mainTbl, i, false);
	FrmShowObject(form, FrmGetObjectIndex(form, labelID_wait));
      }
      FrmDrawForm(form);
      WinSetDrawWindow(FrmGetWindowHandle(form));
      MakeHelpButton(form);

      if (dbCount <= 0 || dbListFull)
	LoadDBInfo();
      SetDBView();
      FrmHideObject(form, FrmGetObjectIndex(form, labelID_wait));

      TblGetBounds(mainTbl, &rect);
      rowHeight = rect.extent.y / mainTblRows;
      TblSetColumnUsable(mainTbl, colBackup, true);
      TblSetColumnUsable(mainTbl, colDBName, true);
      TblSetColumnUsable(mainTbl, colDBSize, true);
      TblSetCustomDrawProcedure(mainTbl, colDBName, TblDrawString);
      TblSetCustomDrawProcedure(mainTbl, colDBSize, TblDrawString);
      for (i = 0; i < mainTblRows; i++) {
	TblSetRowHeight(mainTbl, i, rowHeight);
	TblSetRowData(mainTbl, i, 0);
	// TblSetRowID(mainTbl, i, i);

	TblSetItemStyle(mainTbl, i, colBackup, checkboxTableItem);
	TblSetItemStyle(mainTbl, i, colDBName, customTableItem);
	TblSetItemStyle(mainTbl, i, colDBSize, customTableItem);
      }
      UpdateDBTable(mainTbl, mainTblTop, true);

      handled = true;
      break;

    case keyDownEvent:
      switch (event->data.keyDown.chr) {
	case pageUpChr:
	  UpdateDBTable(mainTbl, mainTblTop - mainTblRows, false);
	  handled = true;
	  break;
	case pageDownChr:
	  UpdateDBTable(mainTbl, mainTblTop + mainTblRows, false);
	  handled = true;
	  break;
      }

    case ctlSelectEvent:  // A control button was pressed.
    case ctlRepeatEvent:  // A repeating button was held down.
      form = FrmGetActiveForm();
      id = (event->eType == ctlSelectEvent) ?
	event->data.ctlSelect.controlID :
	event->data.ctlRepeat.controlID;
      switch (id) {
	case buttonID_up:
	  UpdateDBTable(mainTbl, mainTblTop - 1, false);
	  if (event->eType == ctlRepeatEvent)
	    event->data.ctlRepeat.time = SysTicksPerSecond() / 2;
	  else
	    handled = true;
	  break;
	case buttonID_down:
	  UpdateDBTable(mainTbl, mainTblTop + 1, false);
	  if (event->eType == ctlRepeatEvent)
	    event->data.ctlRepeat.time = SysTicksPerSecond() / 2;
	  else
	    handled = true;
	  break;
	case boxID_apps:
	  prefs.viewApps =
	    CtlGetValue((ControlPtr)event->data.ctlSelect.pControl) ? 1 : 0;
	  SetDBView();
	  UpdateDBTable(mainTbl, 0, true);
	  handled = true;
	  break;
	case buttonID_reload:
	  dbCount = 0;
	  FrmUpdateForm(formID_main, 0);
	  handled = true;
	  break;
	case buttonID_help:
	  FrmHelp(helpStr_main);
	  break;
      }
      break;

    case tblSelectEvent:  // A table item was selected
      if ((db = (DBInfo*)TblGetRowData(mainTbl, event->data.tblSelect.row)) != 0) {
	switch (event->data.tblSelect.column) {
	  case colBackup:
	    n = TblGetItemInt(mainTbl, event->data.tblSelect.row,
	      event->data.tblSelect.column);
	    if (FrmCustomAlert(alertID_confirm,
	      "change this backup bit", 0, 0) == 1) {
		if (!SetBackup(db, (n == 0) ? false : true)) {
		  FrmAlert(alertID_backupErr);
		  UpdateDBTable(mainTbl, mainTblTop, true);
		}
	      }
	      else {
		UpdateDBTable(mainTbl, mainTblTop, true);
	      }
	    break;
	  case colDBName:
	    selectDB = db;
	    FrmPopupForm(formID_dbView);
	    break;
	  default:
	    TblUnhighlightSelection(mainTbl);
	    break;
	}
      }
      handled = true;
      break;

    case popSelectEvent:  // A selector item was selected
      form = FrmGetActiveForm();
      switch (event->data.popSelect.controlID) {
	case triggerID_view:
	  if (event->data.popSelect.selection != prefs.view) {
	    prefs.view = event->data.popSelect.selection;
	    SetSelector(form, listID_view, triggerID_view, prefs.view);
	    SetDBView();
	    UpdateDBTable(mainTbl, 0, true);
	  }
	  handled = true;
	  break;
      }
      break;

    case menuEvent:
      handled = HandleMenu(event->data.menu.itemID);
      break;
  }
  return handled;
}

static char *
TypeStr(ULong val)
{
  static char buf[5];

  buf[0] = (char)(val >> 24);
  buf[1] = (char)(val >> 16);
  buf[2] = (char)(val >> 8);
  buf[3] = (char)val;
  buf[4] = '\0';
  return buf;
}

static Boolean
DbViewForm(EventPtr event)
{
  FormPtr   form;
  Boolean   handled = false;
  int       id, row, col, i;
  char      buf[32], *s, *save;
  ULong     size, origSize, crDate;
  UInt      card;
  LocalID   lid;
  DmSearchStateType state;
  static Boolean systemDB;
  ControlPtr ctl;
  DateTimeType dt;

  switch (event->eType)
  {
    case frmOpenEvent:
      form = FrmGetActiveForm();
      if (selectDB->compressed) {
	for (i = 0; i < 10; i++)
	  FrmHideObject(form, FrmGetObjectIndex(form, level_0 + i));
	FrmHideObject(form, FrmGetObjectIndex(form, labelID_level));
	FrmHideObject(form, FrmGetObjectIndex(form, labelID_faster));
	FrmHideObject(form, FrmGetObjectIndex(form, labelID_better));

	ctl = (ControlPtr)FrmGetObjectPtr(form,
	  FrmGetObjectIndex(form, boxID_delete));
	CtlSetLabel(ctl, "Delete after uncompress");
	CtlSetValue(ctl, prefs.uncompDelete);

	ctl = (ControlPtr)FrmGetObjectPtr(form,
	  FrmGetObjectIndex(form, boxID_data));
	CtlSetLabel(ctl, "Uncompress all data also");
	CtlSetValue(ctl, prefs.uncompData);

	CtlSetLabel((ControlPtr)FrmGetObjectPtr(form,
	  FrmGetObjectIndex(form, buttonID_compress)), "Uncompress");
      }
      else {
	CtlSetValue((ControlPtr)FrmGetObjectPtr(form,
	  FrmGetObjectIndex(form, boxID_delete)), prefs.compDelete);
	CtlSetValue((ControlPtr)FrmGetObjectPtr(form,
	  FrmGetObjectIndex(form, boxID_data)), prefs.compData);
      }
      if (selectDB->type != 'appl')
	FrmHideObject(form, FrmGetObjectIndex(form, boxID_data));
      FrmDrawForm(form);
      WinSetDrawWindow(FrmGetWindowHandle(form));
      MakeHelpButton(form);

      FrmSetControlGroupSelection(form, groupID_level,
	level_0 + prefs.level);

      row = 15;
      col = 45;
      Print(0, row, "Name:", boldFont);
      Print(col, row, selectDB->name, stdFont);
      row += 11;

      Print(0, row, "Type:", boldFont);
      if (selectDB->type == 'appl')
	Print(col, row, "Application", stdFont);
      else {
	if (selectDB->resourceDB)
	  StrPrintF(buf, "Resource (%s)", TypeStr(selectDB->type));
	else
	  StrPrintF(buf, "Data (%s)", TypeStr(selectDB->type));
	Print(col, row, buf, stdFont);
      }
      row += 11;

      //
      // 3com reserves creator ID's of all lower-case letters
      // for internal use.  We use this as a makeshift indicator
      // that a database is for system use and probably shouldn't
      // be compressed.
      //
      for (s = save = TypeStr(selectDB->creator); *s; s++)
	if (*s < 'a' || *s > 'z')
	  break;
      systemDB = (*s) ? false : true;
      if (selectDB->type != 'appl') {
	Print(0, row, "Creator:", boldFont);
	if (DmGetNextDatabaseByTypeCreator(true, &state, 'appl',
	  selectDB->creator, true, &card, &lid) != 0
	 || DmDatabaseInfo(card, lid, buf, 0, 0, 0, 0, 0,
	  0, 0, 0, 0, 0) != 0) {
	    if (systemDB)
	      StrPrintF(buf, "System");
	    else
	      StrPrintF(buf, "Unknown (%s)", save);
	}
	buf[32] = '\0';	// just in case
	Print(col, row, buf, stdFont);
	row += 11;
      }

      Print(0, row, "Size:", boldFont);
      StrPrintF(buf, "%ld bytes%s", selectDB->size,
	selectDB->compressed ? " compressed" : "");
      Print(col, row, buf, stdFont);
      row += 11;

      if (selectDB->compressed && selectDB->origSize > 0) {
	StrPrintF(buf, "%ld uncompressed", selectDB->origSize);
	Print(col, row, buf, stdFont);
	row += 11;
      }

      if (selectDB->type == 'appl') {
	Print(0, row, "Data:", boldFont);
	size = origSize = 0;
	for (i = 0; i < dbCount; i++)
	  if (dbList[i].valid
	    && dbList[i].type != 'appl'
	    && dbList[i].creator == selectDB->creator
	    && dbList[i].compressed == selectDB->compressed) {
	      size += dbList[i].size;
	      origSize += dbList[i].origSize;
	    }
	StrPrintF(buf, "%ld bytes%s", size,
	  selectDB->compressed ? " compressed" : "");
	Print(col, row, buf, stdFont);
	row += 11;
	if (selectDB->compressed && origSize > 0) {
	  StrPrintF(buf, "%ld uncompressed", origSize);
	  Print(col, row, buf, stdFont);
	  row += 11;
	}
      }

      if (selectDB->compressed
       && DmDatabaseInfo(selectDB->card, selectDB->id, 0, 0,
	0, &crDate, 0, 0, 0, 0, 0, 0, 0) == 0) {
	Print(0, row, "Date:", boldFont);
	TimSecondsToDateTime(crDate, &dt);
	DateToAscii(dt.month, dt.day, dt.year, dfDMYLong, buf);
	for (i = 0; buf[i]; i++)
	  continue;
	buf[i++] = ' ';
	buf[i++] = ' ';
	TimeToAscii(dt.hour, dt.minute, tfColon24h, buf + i);
	Print(col, row, buf, stdFont);
	row += 11;
      }

      handled = true;
      break;

    case ctlSelectEvent:  // A control button was pressed.
      form = FrmGetActiveForm();
      id = event->data.ctlSelect.controlID;
      if (id >= level_0 && id <= level_9)
	prefs.level = id - level_0;
      else
	switch (id) {
	  case buttonID_done:
	    FrmReturnToForm(formID_main);
	    handled = true;
	    break;

	  case buttonID_compress:
	    if (systemDB
	     && FrmCustomAlert(alertID_systemDB,
	      "uncompress"+(selectDB->compressed ? 0 : 2),
	      0, 0) != 1)
		break;
	    FrmGotoForm(formID_dbComp);
	    handled = true;
	    break;

	  case buttonID_delete:
	    if (FrmCustomAlert(
	      (systemDB ? alertID_systemDB : alertID_database),
	      "DELETE", 0, 0) != 1)
		break;
	    if (DeleteDB(selectDB)) {
	      FrmUpdateForm(formID_main, 0);
	      FrmReturnToForm(formID_main);
	    }
	    handled = true;
	    break;

	  case boxID_delete:
	    i = CtlGetValue((ControlPtr)
	      event->data.ctlSelect.pControl) ? 1 : 0;
	    if (selectDB->compressed)
	      prefs.uncompDelete = i;
	    else
	      prefs.compDelete = i;
	    handled = true;
	    break;

	  case boxID_data:
	    i = CtlGetValue((ControlPtr)
	      event->data.ctlSelect.pControl) ? 1 : 0;
	    if (selectDB->compressed)
	      prefs.uncompData = i;
	    else
	      prefs.compData = i;
	    handled = true;
	    break;

	  case buttonID_help:
	    FrmHelp(helpStr_dbView);
	    break;
	}
      break;

    case menuEvent:
      handled = HandleMenu(event->data.menu.itemID);
      break;
  }
  return handled;
}

static Boolean
DbCompForm(EventPtr event)
{
  FormPtr   form, mainForm;
  Boolean   handled = false;
  int       id, rc;
  UInt      count;
  char      buf[8], *msg, *note;

  switch (event->eType)
  {
    case frmOpenEvent:
      form = FrmGetActiveForm();
      FrmHideObject(form, FrmGetObjectIndex(form, buttonID_done));
      FrmDrawForm(form);
      WinSetDrawWindow(FrmGetWindowHandle(form));

      buf[0] = buf[2] = buf[4] = buf[6] = 0xAA;  // 10101010
      buf[1] = buf[3] = buf[5] = buf[7] = 0x55;  // 01010101
      WinSetPattern((Word*)buf);

      for (;;) {
	//
	// Perform the compress/uncompress.
	//
	if ((rc = ((selectDB->compressed) ?
	  Uncompress(selectDB, &count) :
	  Compress(selectDB, &count))) != rcNoMem)
	    break;
	if (count > 0)		// compress was partially successful
	  break;

	//
	// Perhaps the compression algorithm can't get enough memory
	// because we're taking up too much with the main form, table,
	// and database list.  Free everything and retry.
	//
	if ((mainForm = FrmGetFormPtr(formID_main)) == 0)
	  break;
	FrmDeleteForm(mainForm);
	ResetDBList();
      }
      if (rc < 0) {
	switch (rc) {
	  case rcNoMem:
	    msg = "process memory is exhausted - try a lower compression level";
	    break;
	  case rcBadData:
	    msg = "the compressed data is invalid or corrupted";
	    break;
	  case rcBadVersion:
	    msg = "the data was compressed by a later version of PCompress";
	    break;
	  case rcCancel:
	    msg = (count == 0) ? 0 : "you cancelled it";
	    break;
	  case rcDmError:
	    msg = "of a database manager error";
	    break;
	  case rcDmLock:
	    msg = "a database or record was unreadable or locked";
	    break;
	  case rcCantCompress:
	    msg = "the data is already an optimal size and cannot be compressed further";
	    break;
	  case rcNoDmMem:
	    msg = "there is not enough memory";
	    break;
	  default:
	    msg = "of an internal or system error";
	}
	if (count > 0)
	  note = "\n\n**NOTE** Some databases processed successfully.";
	else
	  note = "";
	if (msg)
	  FrmCustomAlert(alertID_rcFail, msg, note, 0);
      }

      //
      // Update the progress meter.
      //
      if (rc < 0)
	ProgressMeter((rc == rcCancel) ? "Cancelled" : "Failed");
      else {
	progByteDone = progByteTotal = 100;
	progEncTotal = 0;
	ProgressComplete(count);
	ProgressMeter("Complete");
      }
      FrmHideObject(form, FrmGetObjectIndex(form, buttonID_cancel));
      FrmShowObject(form, FrmGetObjectIndex(form, buttonID_done));
      handled = true;
      break;

    case ctlSelectEvent:  // A control button was pressed.
      id = event->data.ctlSelect.controlID;
      switch (id) {
	case buttonID_done:
	  if (FrmGetFormPtr(formID_main) == 0)
	    FrmGotoForm(formID_main);
	  else {
	    FrmUpdateForm(formID_main, 0);
	    FrmReturnToForm(formID_main);
	  }
	  handled = true;
	  break;
	case buttonID_cancel:
	  handled = true;
	  break;
      }
      break;
  }
  return handled;
}

//
// We define our own Random function because some certain PalmOS
// functions seem to use SysRandom() and screw up the ability to
// seed the generator and get the same string of random numbers.
//
static ULong
Random(ULong seed)
{
  static ULong word[4];

  if (seed) {
    word[0] = seed;
    word[1] = word[0] * 100100107;
    word[2] = word[1] * 100100107 + 80370;
  }
  else {
    word[3] = word[0] + word[2];
    word[0] = word[1];
    word[1] = word[2];
    word[2] = word[3];
  }
  return (word[3] >> 2);
}

static int
FillData(VoidPtr ptr, int len, Boolean writing)
{
  int i, copyPos, copyLen, index = 0;
  char buf[1024], *b, *p, *end;

  while (len > 0) {
    copyLen = 0;
    for (i = 0; i < sizeof(buf) && i < len; i++) {
      if (copyLen > 0) {
	buf[i] = buf[copyPos++];
	copyLen--;
      }
      else {
	buf[i] = Random(0) & 0xff;
	if ((Random(0) % 10) == 0) {
	  copyPos = Random(0) % (i+1);
	  copyLen = (Random(0) % 20) + 3;
	}
      }
    }
    if (writing) {
      if (DmWrite(ptr, index, buf, i) != 0)
	return rcDmError;
    }
    else {
      end = buf + i;
      for (b = buf, p = (char*)ptr + index; b < end; b++, p++)
	if (*b != *p)
	  return 50;
    }
    len -= i;
    index += i;
  }
  return 0;
}

static int forever;

static int
RunTest(void)
{
  ULong seed, loop, recID, recIDCmp, resType, resTypeCmp;
  UInt card, at, recAttr, recAttrCmp, count;
  Int resID, resIDCmp;
  LocalID id, chunkID, appID, sortID;
  char buf[80], name[32], opt[10];
  int row, recs, i, len, rc, n, dbs, dbNum;
  Boolean writing, resource;
  DBInfo *db;
  VoidHand hand;
  VoidPtr ptr;

  //
  // Set up the preferences for testing.  The caller should take
  // care of restoring the prefs to their original values.
  //
  prefs.level = 2;
  prefs.compDelete = true;
  prefs.compData = true;
  prefs.uncompDelete = true;
  prefs.uncompData = true;
  VectorMax = 10;

  for (loop = 1; CheckCancel() == 0; loop++) {
    //
    // Set up test variables.
    //
    if (!forever && loop > 10)
      break;
    ResetDBList();
    row = 15;
    Clear(0, row, 160, 145-row);
    Random(TimGetTicks());
    do {
      seed = Random(0) & (ULong)0x7fffffff;
    } while (seed == 0);
    StrPrintF(buf, "Seed = %lu, Loop = %lu%s",
      seed, loop, (forever ? "" : " of 10"));
    Print(0, row, buf, stdFont);
    row += 11;

    for (writing = true;; writing = false) {
      //
      // Seed random number generator.
      //
      Random(seed);

      //
      // Create some number (1..3) of databases.
      //
      dbs = (Random(0) % 3) + 1;
      for (dbNum = 0; dbNum < dbs; dbNum++) {
	StrPrintF(name, "PComp Auto Test DB %d", dbNum);
	if (writing && (id = DmFindDatabase(0, name)) != 0)
	  if (DmDeleteDatabase(0, id) != 0)
	    return 1;
	resource = ((dbNum == 0 && dbs > 1) || (Random(0) & 0x10));
	if (writing) {
	  if (DmCreateDatabase(0, name, pcompCreator, 'test', resource) != 0
	   || (id = DmFindDatabase(0, name)) == 0)
	    return 2;
	  appID = sortID = 0;
	}
	else {
	  db = &dbList[dbNum];
	  id = db->id;
	  if (resource != db->resourceDB)
	    return 31;
	  if (DmDatabaseInfo(db->card, db->id, 0, 0,
	    0, 0, 0, 0, 0, &appID, &sortID, 0, 0) != 0)
	      return rcDmError;
	}
	if ((outDB = DmOpenDatabase(0, id, dmModeReadWrite)) == 0)
	  return 3;

	//
	// Handle data records.
	//
	recs = (Random(0) % 30) + 5;
	StrPrintF(buf, "%s DB %d: %s %d records...",
	  (resource ? "Rsrc" : "Data"), dbNum,
	  (writing ? "Writing" : "Checking"), recs);
	Print(0, row, buf, stdFont);
	if (!writing && recs != DmNumRecords(outDB))
	  return 32;
	for (i = 0; i < recs; i++) {
	  n = Random(0) % 100;
	  if (n >= 95)
	    len = (Random(0) % 25000) + 500;
	  else if (n >= 60)
	    len = (Random(0) % 500) + 10;
	  else
	    len = (Random(0) % 50) + 2;
	  if (resource) {
	    resID = 1000 + i;
	    n = Random(0) % 1000;
	    ((char*)&resType)[0] = (n % 26) + 'a';
	    ((char*)&resType)[1] = ((n * 3) % 26) + 'a';
	    ((char*)&resType)[2] = ((n * 5) % 26) + 'a';
	    ((char*)&resType)[3] = ((n * 7) % 26) + 'a';
	    if (writing)
	      hand = DmNewResource(outDB, resType, resID, len);
	    else {
	      if (DmResourceInfo(outDB, i, &resTypeCmp, &resIDCmp, 0) != 0)
		return 35;
	      if (resTypeCmp != resType || resIDCmp != resID)
		return 36;
	      hand = DmGetResourceIndex(outDB, i);
	    }
	  }
	  else {
	    at = i;
	    recID = 1000 + i;
	    recAttr = (int)(Random(0) % 256)
	      & ~(dmRecAttrDelete|dmRecAttrBusy);
	    if (writing)
	      hand = DmNewRecord(outDB, &at, len);
	    else {
	      if (DmRecordInfo(outDB, i, &recAttrCmp, &recIDCmp,
		&chunkID) != 0)
		  return 35;
	      //
	      // Don't check record ID and attributes yet, since they
	      // may be modified below by record archiving or deleting.
	      //
	      if (!chunkID)
		hand = 0;
	      else
		hand = (VoidHand)MemLocalIDToGlobal(chunkID, db->card);
	    }
	  }
	  if (resource || (Random(0) % 15) != 0) {
	    if (hand == 0)
	      return (writing ? rcNoDmMem : 37);
	    if (!writing && MemHandleSize(hand) != len)
	      return 38;

	    //
	    // Fill in record data.
	    //
	    ptr = MemHandleLock(hand);
	    rc = FillData(ptr, len, writing);
	    MemHandleUnlock(hand);
	    if (rc != 0)
	      return rc;
	    if (!resource) {
	      if (writing
	       && DmSetRecordInfo(outDB, at, &recAttr, &recID) != 0)
		return 5;
	      if ((Random(0) % 5) == 0) {
		//
		// Make this an archived record.
		//
		if (!writing)
		  recAttr |= (dmRecAttrDelete |
		    (recAttrCmp & dmRecAttrDirty));
		else if (DmArchiveRecord(outDB, at) < 0)
		  return 6;
	      }
	    }
	  }
	  else {
	    //
	    // Make this a deleted record.
	    //
	    if (!writing) {
	      if (hand)
		return 39;
	      recAttr |= (dmRecAttrDelete |
		(recAttrCmp & dmRecAttrDirty));
	    }
	    else {
	      if (hand == 0)
		return rcNoDmMem;
	      if (DmSetRecordInfo(outDB, at, &recAttr, &recID) != 0)
		return 5;
	      if (DmDeleteRecord(outDB, at) < 0)
		return 8;
	    }
	  }
	  if (!resource && !writing
	   && (recAttrCmp != recAttr || recIDCmp != recID))
	    return 36;
	}

	//
	// Handle appInfo and sortInfo.
	//    i == 0 : appInfo
	//    i == 1 : sortInfo
	//
	n = 0;
	for (i = 0; i < 2; i++) {
	  if ((Random(0) % 3) == 0) {
	    len = (Random(0) % 4000) + 2;
	    if (writing)
	      hand = DmNewHandle(outDB, len);
	    else {
	      if ((i == 0 && !appID) || (i == 1 && !sortID))
		return 41;
	      hand = (VoidHand)MemLocalIDToGlobal(
		((i == 0) ? appID : sortID), db->card);
	    }
	    if (hand == 0)
	      return (writing ? rcNoDmMem : 42);
	    if (!writing && MemHandleSize(hand) != len)
	      return 43;
	    ptr = MemHandleLock(hand);
	    rc = FillData(ptr, len, writing);
	    MemHandleUnlock(hand);
	    if (rc != 0)
	      return rc;

	    if (writing) {
	      if (i == 0) {
		appID = MemHandleToLocalID(hand);
		if (DmSetDatabaseInfo(0, id, 0, 0,
		  0, 0, 0, 0, 0, &appID, 0, 0, 0) != 0)
		    return 45;
	      }
	      else {
		sortID = MemHandleToLocalID(hand);
		if (DmSetDatabaseInfo(0, id, 0, 0,
		  0, 0, 0, 0, 0, 0, &sortID, 0, 0) != 0)
		    return 45;
	      }
	    }

	    if (n == 0) {
	      opt[n++] = ' ';
	      opt[n++] = '+';
	    }
	    opt[n++] = ((i == 0) ? 'A' : 'S');
	  }
	  else if (!writing) {
	    if ((i == 0 && appID) || (i == 1 && sortID))
	      return 48;
	  }
	}
	opt[n] = '\0';

	//
	// Close and register this database.
	//
	DmCloseDatabase(outDB), outDB = 0;
	if (writing) {
	  if ((db = NewDBInfo(0, id, 5)) == 0)
	    return 9;
	  if (dbNum == 0 && dbs > 1)
	    db->type = 'appl';	// make it look like an application
	}

	StrPrintF(buf, "%s DB %d: %d recs%s, %ld bytes",
	  (resource ? "Rsrc" : "Data"), dbNum, recs, opt, db->size);
	Print(0, row, buf, stdFont);
	row += 11;

	if (CheckCancel() != 0)
	  return 0;
      }

      //
      // Compress the databases.
      //
      if (!writing)
	break;
      Print(0, row, "Compressing...", stdFont);
      if ((rc = Compress(&dbList[0], &count)) < 0)
	return rc;
      if (count != dbs)
	return 11;
      for (dbNum = 0; dbNum < dbs; dbNum++) {
	db = &dbList[dbNum];
	if (!db->compressed)
	  return 12;
	if (dbNum == 0 && dbs > 1)
	  db->type = 'appl';	// make it look like an application
	StrPrintF(buf, "DB %d shrank from %ld to %ld",
	  dbNum, db->origSize, db->size);
	Print(0, row, buf, stdFont);
	row += 11;
      }
      if (CheckCancel() != 0)
	return 0;

      //
      // Uncompress the databases.
      //
      Print(0, row, "Uncompressing...", stdFont);
      if ((rc = Uncompress(&dbList[0], &count)) < 0)
	return rc;
      if (count != dbs)
	return 21;
      if (CheckCancel() != 0)
	return 0;
    }

    Print(0, row, "Test Successful", stdFont);
    for (i = 0; i < dbCount; i++)
      if (dbList[i].creator == pcompCreator) {
	DmDeleteDatabase(dbList[i].card, dbList[i].id);
	dbList[i].creator = 0;
      }
    SysTaskDelay(SysTicksPerSecond());
  }
  return 0;
}

static Boolean
TestForm(EventPtr event)
{
  FormPtr   form;
  Boolean   handled = false;
  int       rc, i;
  char      buf[20];
  struct _prefs savePrefs;

  switch (event->eType)
  {
    case frmOpenEvent:
      form = FrmGetActiveForm();
      FrmDrawForm(form);
      WinSetDrawWindow(FrmGetWindowHandle(form));

      forever = 0;
      ResetDBList();
      savePrefs = prefs;
      testing = true;
      rc = RunTest();
      testing = false;
      if (outDB)
	DmCloseDatabase(outDB), outDB = 0;
      for (i = 0; i < dbCount; i++)
	if (dbList[i].creator == pcompCreator)
	  DmDeleteDatabase(dbList[i].card, dbList[i].id);
      prefs = savePrefs;
      VectorMax = VectorMaxDflt;
      ResetDBList();

      if (rc == 0) {
	FrmUpdateForm(formID_main, 0);
	FrmReturnToForm(formID_main);
      }
      else {
	if (rc == rcNoDmMem)
	  StrPrintF(buf, "FAIL - not enough free memory");
	else
	  StrPrintF(buf, "FAIL, rc = %d", rc);
	Print(0, 148, buf, stdFont);
      }
      handled = true;
      break;

    case ctlSelectEvent:
      switch (event->data.ctlSelect.controlID) {
	case buttonID_cancel:
	  FrmUpdateForm(formID_main, 0);
	  FrmReturnToForm(formID_main);
	  handled = true;
	  break;

	case boxID_forever:
	  forever = CtlGetValue((ControlPtr)event->data.ctlSelect.pControl);
	  handled = true;
	  break;
      }
      break;
  }
  return handled;
}

static Boolean
AppHandleEvent(EventPtr event)
{
  int formID;
  FormPtr form;
  Boolean handled = false;

  switch (event->eType) {
    case frmLoadEvent:
      formID = event->data.frmLoad.formID;
      form = FrmInitForm(formID);
      FrmSetActiveForm(form);
      switch (formID) {
	case formID_main:
	  FrmSetEventHandler(form, (FormEventHandlerPtr) MainForm);
	  break;
	case formID_dbView:
	  FrmSetEventHandler(form, (FormEventHandlerPtr) DbViewForm);
	  break;
	case formID_dbComp:
	  FrmSetEventHandler(form, (FormEventHandlerPtr) DbCompForm);
	  break;
	case formID_test:
	  FrmSetEventHandler(form, (FormEventHandlerPtr) TestForm);
	  break;
      }
      handled = true;
      break;
  }
  return handled;
}

static void
EventLoop(void)
{
  short err;
  EventType event;

  do {
    EvtGetEvent(&event, evtWaitForever);

    if (SysHandleEvent(&event))
      continue;
    if (MenuHandleEvent(NULL, &event, &err))
      continue;
    if (AppHandleEvent(&event))
      continue;
    FrmDispatchEvent(&event);

  } while (event.eType != appStopEvent);
}

static void
StopApplication(void)
{
  if (pCtl)
    PCompClose(pRef, pCtl);
#ifdef PCOMP_SHLIB
  PCompLibClose(pRef);
#endif
  SavePrefs();
}

DWord
PilotMain(Word cmd, Ptr cmdPBP, Word launchFlags)
{
  int error;

  if (cmd == sysAppLaunchCmdNormalLaunch) {
    if ((error = StartApplication()) != 0)
      return error;

    EventLoop();	// Event loop

    StopApplication();	// Application stop code
  }
  return 0;
}
