/* MRBackup - Amiga Hard Disk Backup Utility
 * Filename:	Main.c
 * Author:		Mark R. Rinfret
 * Date:		08/01/87
 *
 * This program has been contributed to the public domain.  It represents
 * a collection of original work and other public domain offerings that 
 * were found to be useful in writing this application.  To the best of
 * my knowledge, this program works as described in the accompanying
 * document, but no warranties are made in this regard.
 * USE AT YOUR OWN RISK.
 *
 * If you find this program useful, or if you have comments or suggestions
 * for enhancing it, please send email to mark@unisec.USI.COM or U.S. Mail
 * to:
 *		Mark R. Rinfret
 *		348 Indian Avenue
 *		Portsmouth, RI  02871
 *		401-846-7639 (home)
 *		401-849-4174 (work)
 *
 * History:		(most recent change first)
 *
 * 09/04/87 -MRR- V1.3: Extracted routines related to the backup function
 *                and placed them in package Backup.c.
 *
 * 09/03/87 -MRR- V1.3: Fixed a bug in the routines which handle the
 *                listing file and pathname.
 *				  Extended the IsCompressed() function to check for
 *                .ARC and .ZOO files.
 *
 * 08/22/87 -MRR- V1.2: Extracted global data and definitions and placed
 *                them in a common include file.
 *
 * 08/11/87 -MRR- V1.1: BUG!  The variable 'back' (backup sequence number) 
 *				  was not set to 0 by Backup().
 */

#define MAIN

#include "MRBackup.h"
extern struct Requester *pathrequest;
extern struct Window *pathwindow;


/* Main program  - N.S.D.T.! */

main(argc,argv)
int     argc;
char   *argv[];
{

	Initialize();
	User();					/* it's in the user's hands */
	CleanUp(NULL, 0);
}

/* Initialize the program. */

Initialize()
{
	if (! (IntuitionBase = (struct IntuitionBase *)
			OpenLibrary("intuition.library", 33L ) ) ) {
		CleanUp("Can't open Intuition library!", 20);
	}

	if (!( mywindow = OpenWindow(&nw) ) )
		CleanUp("Can't open program window!", 20);

#ifdef DEBUG
	if (!(debugconsole = 
		Open("CON:0/100/640/40/Debug Info", MODE_NEWFILE))) 
		CleanUp("Can't open debug console!", 20);
#endif

	if (!(console = 
		Open("CON:0/100/640/90/Progress Report", MODE_NEWFILE)))
		CleanUp("Can't open console!", 20);

	SetMenuStrip(mywindow, &Titles[0]);

	InitPathRequest();

	AddGadget(mywindow, &StopGad, -1L);
	OnGadget(&StopGad, mywindow, NULL);

	now = (struct DateStamp *) 
		AllocMem((long) sizeof(struct DateStamp), MEMF_PUBLIC);

	since = (struct DateStamp *)
		AllocMem( (long) sizeof(struct DateStamp), MEMF_PUBLIC);

	InitBuffer();					/* Allocate copy/compress buffer */
	GetUserPrefs();					/* Get user preferences */
	SetSpeech();
}

/* Allocate the buffer used for copy/compress operations. */

InitBuffer()
{
	for (bufsize = BUFMAX; bufsize > 2048; bufsize -= 2048 )
		if (buffer = AllocMem(bufsize, MEMF_CHIP))
			return;

	CleanUp("Not enough memory for copy buffer!\n",20);
}

/* Handle program termination.
 * Called with:
 *		msg:		termination message or NULL
 *		code:	 	exit code (0 => normal termination)
 */

CleanUp(msg, code)
	char *msg; int code;
{
	if (msg)
		puts(msg);

#ifdef DEBUG
	if (debugconsole) Close(debugconsole);
#endif

	if (console) Close(console);

	if (listing) fclose(listing);

	if (mywindow) {
		ClearMenuStrip(mywindow);
		CancelPathRequest();
		CloseWindow(mywindow);
	}

	if (IntuitionBase) CloseLibrary(IntuitionBase);
	if (now) FreeMem( now, (long) sizeof(struct DateStamp));
	if (since) FreeMem( since, (long) sizeof(struct DateStamp));
	if (buffer) FreeMem(buffer, bufsize);
	exit(code);
}

/* Break a full file specification into its volume and pathname components.
 * Called with:
 *		fullpath:	full pathname string (input)
 *		volume:		volume name component, sans colon (output)
 *		path:		pathname component (output)
 * Returns:
 *		status code: 1 => no volume name specified, 0 otherwise
 */

BreakPath(fullpath,volume,path)
	char *fullpath, *volume, *path;
{
	char c, *fp, *s;
	unsigned has_volume = 1;			/* assume it has volume component */

	fp = fullpath;
	s = volume;
	if ( index(fp, ':') ) {				/* volume name specified? */
		s = volume;
		while ((c = *fp++) != ':') *s++ = c;
	}
	else
		has_volume = 0;

	*s = '\0';							/* terminate volume */
	strcpy(path, fp);					/* the rest is pathname stuff */
	return has_volume;
}

/* Do a quick poll on the STOP gadget.
 * Returns 1 if STOP gadget hit, 0 otherwise.
 */

int
CheckStop()
{
	ULONG class;
	struct Gadget *gadget;
	struct IntuiMessage *msg;
	int status = 0;

	if (msg = (struct IntuiMessage *) GetMsg(mywindow->UserPort)) {
		class = msg->Class;
		gadget = (struct Gadget *) msg->IAddress;
		ReplyMsg(msg);
		if (class == GADGETUP && gadget->GadgetID == STOPGAD) {
			TypeAndSpeak("I am stopping, as you requested.\n");
			status = ERR_ABORT;		/* quit */
		}
	}
	return status;
}

#ifdef DEBUG
DebugWrite(msg)
	char *msg;
{
	Write(debugconsole, msg, (long) strlen(msg));
}
#endif

/* Handle a gadget action.
 * Called with:
 *      window:		window that gadget is displayed in
 *		class:		message class (GADGETUP/GADGETDOWN/?)
 *		addr:		pointer to gadget structure
 */
DoGadget(window, class, addr)
	struct Window *window; ULONG class; struct Gadget *addr;
{
	USHORT id;					/* gadget identifier */
	struct Gadget *upgad;
	ULONG upclass;
	struct IntuiMessage *upmsg;	/* require gadget up to complete */

	id = addr->GadgetID;

#ifdef TESTING
	if (class == GADGETDOWN && window == pathwindow) {/* pathname gadget */
		ActivateWindow(pathwindow);
		do {
			Wait(1L << pathwindow->UserPort->mp_SigBit);
			if (upmsg = (struct IntuiMessage *) 
				GetMsg(pathwindow->UserPort)) {
				upclass = upmsg->Class;
				upgad = (struct Gadget *) upmsg->IAddress;
				ReplyMsg(upmsg);
				if (upclass == GADGETUP && upgad->GadgetID == id) {
					class = GADGETUP;	/* use new class */
				}
				else {
					upmsg = NULL;
					ActivateGadget(addr, pathwindow, pathrequest);
				}
			}
		}
		while (! upmsg );
	}
#endif

	if (class == GADGETUP) {
#ifdef DEBUG
		sprintf(debugmsg,"GADGETUP: %d\n",id);
		DebugWrite(debugmsg);
#endif
		switch (id) {
			case HOMEPATHGAD:
				GetHomePath(addr);
				break;
			case BACKPATHGAD:
				GetBackPath(addr);
				break;
			case LISTPATHGAD:
				GetListPath(addr);
				break;
			case XCLDPATHGAD:
				GetXcldPath(addr);
				break;
			case STOPGAD:
				TypeAndSpeak(
	"Use the STOP gadget during backup and restore operations.\n");
				break;
			default:
				TypeAndSpeak("Unknown gadget - program error!\n");
				break;
		}
	}
}

/* Get the backup device pathname specification. 
 * Called with:
 * Called with:
 *		gadget:		pointer to relevant string gadget
 *
 * Side-effects:
 *		If the pathname specification passes its requirements tests,
 *		the global string backpath will be set to the pathname.
 *		Otherwise, homepath is left unchanged and the gadget's string
 *		is reset to the current contents of homevolume.
 */

GetBackPath(gadget)
	struct Gadget *gadget;
{
	char volume[VOLUME_MAX+1];
	UBYTE *fullpath, path[PATH_MAX+1];

	fullpath = (UBYTE *) GadgetString(gadget);

	BreakPath(fullpath,volume,path);
	if (strlen(volume) != 3 ||
		tolower(volume[0]) != 'd' ||
		tolower(volume[1]) != 'f' ||
		volume[2] < '0' || volume[2] > '3') {
		TypeAndSpeak("Backup device must be DF0 through DF3\n");
		strcpy(fullpath, backpath);	/* restore previous value */
	}
	else
		strcpy(backpath, fullpath);		/* use new value */

	RefreshGadgets(gadget, pathwindow, NULL);
}

/* An error has occurred.  Find out what the user wants to do about it.
 * Called with:
 *		msg:		message string
 *		options:	the "set" of options available
 *					0 => sorry - no options, ERR_ABORT returned
 * Returns:
 *		error recovery option
 */

int
GetErrOpt(msg, options)
	char *msg; int options;
{
	int i, mask, option_count = 0, select;
	int option_list[NERRCODE];

	sprintf(conmsg,"An error has interrupted processing:\n%s\n\n",msg);
	TypeAndSpeak(conmsg);
	return ERR_ABORT;				/* ...implement recovery... */
}

/* Get the home pathname specification (volume and pathname).
 * Called with:
 *		gadget:		pointer to relevant string gadget
 *
 * Side-effects:
 *		If the pathname specification passes its requirements tests,
 *		the global string homepath will be set to the pathname.
 *		Otherwise, homevolume is left unchanged and the gadget's string
 *		is reset to the current contents of homevolume.
 */

GetHomePath(gadget)
	struct Gadget *gadget;
{
	UBYTE *fullpath, path[PATH_MAX+1], volume[VOLUME_MAX+1];

	RemoveGadget(pathwindow, gadget);
	fullpath = (UBYTE *) GadgetString(gadget);

	BreakPath(fullpath, volume, path);
	if (!IsDir(fullpath)) {
badpath:
		TypeAndSpeak(
	"Home path must be a disk device with optional directory name!\n");
		strcpy(fullpath, homepath);	/* restore previous value */
	}
	else if (strlen(volume) != 3 ||
			tolower(volume[0]) != 'd' ||
			(tolower(volume[1]) != 'h' && tolower(volume[1]) != 'f') ||
			!isdigit(volume[2])) {
#ifdef DEBUG
			sprintf(debugmsg,"home device = \"%s\" ? \n",volume);
			DebugWrite(debugmsg);
#endif
			goto badpath;
	}
	else {
		strcpy(homepath, fullpath);
		sprintf(conmsg,"Home path is %s\n", homepath);
		WriteConsole(conmsg);
	}
	ResetStringInfo(gadget->SpecialInfo);
	AddGadget(pathwindow, gadget, -1L);
	RefreshGadgets(gadget, pathwindow, NULL);
}	

/* Get the listing pathname.
 * Called with:
 *		gadget:		pointer to relevant string gadget
 *
 * Side-effects:
 */

GetListPath(gadget)
	struct Gadget *gadget;
{
	UBYTE *path;

	RemoveGadget(pathwindow, gadget);
	path = (UBYTE *) GadgetString(gadget);
	if (!do_listing) {
		TypeAndSpeak("Listing mode is not active.  ");
		TypeAndSpeak("Your change has been ignored.\n");
badpath:
		strcpy(path, listpath);
	}
	else {
		if (strcmp(path, listpath)) {	/* not same pathname */
			if (OpenList(path)) goto badpath;
			strcpy(listpath, path);
			sprintf(conmsg,"Listing path is %s\n", listpath);
			WriteConsole(conmsg);
		}
	}
	ResetStringInfo(gadget->SpecialInfo);
	AddGadget(pathwindow, gadget, -1L);
	RefreshGadgets(gadget, pathwindow, NULL);
}

GetXcldPath(gadget)
	struct Gadget *gadget;
{
	UBYTE *path;

	RemoveGadget(pathwindow, gadget);
	path = (UBYTE *) GadgetString(gadget);
	if (!strcmp(path, excludepath))		/* same pathname */
		return;
	if (access(path, 0)) {				/* can't access the file? */
		TypeAndSpeak("I can't access the exclude file!\n");
		strcpy(path, excludepath);		/* restore the gadget */
	}
	else {
		strcpy(excludepath, path);
		exclude_has_changed = true;
		sprintf(conmsg,"Exclude path is %s\n", excludepath);
	}
	ResetStringInfo(gadget->SpecialInfo);
	AddGadget(pathwindow, gadget, -1L);
	RefreshGadgets(gadget, pathwindow, NULL);
}

/* Output a new header to the listing file. */

Header()
{
	if (do_listing) {
		fprintf(listing,"\f    MRBackup Listing of Volume %s\n\n", destvol);
		linecount = 2;
	}
}

/* Determine if a file is compressed.  This is currently done by looking
 * at the file name suffix.  A rather dumb assessment of the file type
 * is made based on a match to one of these suffixes.
 * 
 * Called with:
 *		filename:	file name string
 * Returns:
 *		1 => file is compressed, 0 => file is not compressed
 */

int     
IsCompressed(filename)
	char *filename;
{
#define NSUFFIX 3			/* number of known suffixes */
typedef struct {
	char *sufx;
	USHORT length;
	} T_SUFFIX;

static T_SUFFIX suffixes[NSUFFIX] = {
	{".z", 2} ,
	{".arc", 4}, 
	{ ".zoo", 4}
	};

	USHORT i, length;
	char *s;

	length = strlen(filename);	/* get length of argument filename */

	for (i = 0; i < NSUFFIX; ++i) {
		if (length < suffixes[i].length) continue;
		/* We're just interested in the current suffix length. */
		s = filename + length - suffixes[i].length;
		if (!strcmpc(s, suffixes[i].sufx))
			return true;		/* suffixes match */
	}
	return false;
}

/* Output a line to the listing file.  Start a new line if necessary.
 * The output string is assumed to have no form control characters.
 * Called with:
 *		str:		string to be output
 */
ListLine(str)
	char *str;
{	
	if (do_listing) {
		if (linecount >= LINES_PER_PAGE) Header();
		fprintf(listing,"  %s\n",str);
		fflush(listing);
		++linecount;
	}
}

/* Create a new directory on the destination disk.
 * Called with:
 *		name:		directory pathname
 * Returns:
 *		false => success
 *		true  => failure
 * Notes:
 *		NewDir may be called by NewDisk() or BackupDirs().  You should
 *		note that a condition of mutual recursion between CheckSize and
 *		NewDir exists, due to the following:
 *
 *		When NewDir is called from BackupDirs(), it calls CheckSize(),
 *		which in turn calls NewDisk() if there is no space left.
 *		The create_dir parameter to CheckSize() (false) prevents a
 *		secondary call to NewDir() since the job is already being
 *		performed.
 *
 *		When CheckSize() is called as a result of BackupFiles()
 *		processing, the create_dir parameter is true so that NewDir()
 *		will be called if NewDisk() is called, thus creating a
 *		continuation of the current directory.  Confused?  Me too :-).
 */
int
NewDir(name)
	char   *name;
{
	char c;
	struct Lock *dirlock;
	int dirleng;
	int errnum;
	char dirname[256];
	int nameindx = 0, nameleng;

	size--;							/* takes a block for a directory */
	if (CheckSize(false))			/* have room on disk? */
		return 1;

	strcpy(dirname,destvol);		/* start with volume name */
	dirleng = strlen(dirname);
	nameleng = strlen(name);

	/* Parse the pathname, one directory node at a time, creating
	 * directories as needed.
	 */

	while (nameindx < nameleng) {
		if (nameindx)				/* 2nd - nth pass? */
			dirname[dirleng++] = '/'; /* directory separator */
		while ((c = name[nameindx++]) && c != '/')
			dirname[dirleng++] = c;
		dirname[dirleng] = '\0';	/* terminate with null */
		if (dirlock = Lock(dirname,SHARED_LOCK)) /* subdir exists? */
			UnLock(dirlock);
		else {						/* create subdirectory */
			if ((dirlock = CreateDir(dirname))== NULL){
				if ((errnum = IoErr())== ERROR_DIRECTORY_NOT_EMPTY){
					sprintf(conmsg,
						"Directory %s already exists!\n",dirname);
					TypeAndSpeak(conmsg);
				}
				else {
					sprintf(conmsg,
						"ERROR %d: Unable to create directory %s\n",
						errnum,dirname);
					TypeAndSpeak(conmsg);
					return 1;
				}
			}
			else
				UnLock(dirlock);
		}
	}								/* endwhile */
	return 0;
}

/* Skip 'n' lines in the listing file.
 * Called with:
 *		n:		number of lines to skip
 */

NewLine(n)
	int n;
{
	if (do_listing) {
		if (n + linecount >= LINES_PER_PAGE)
			Header();
		else while (n--) {
			fputc('\n', listing);
			++linecount;
		}
	}
}

/* Open the listing file.
 * Called with:
 *		name:		file or device pathname
 * Returns:
 *		status (0 => success)
 */
int
OpenList(name)
	char *name;
{
	int status = 0;

	if (listing) fclose(listing);		/* prior listing file open? */
	if (!(listing = fopen(name, "w"))) {
		status = errno;
		sprintf(conmsg,
				"I can't open the listing file \"%s\", error %d.\n",
				name, status);
		TypeAndSpeak(conmsg);
	}
	else
		linecount = LINES_PER_PAGE;
	return status;
}

/* Reset the variables in a StringInfo structure.
 * Called with:
 *		s:		pointer to a StringInfo
 */
ResetStringInfo(s)
	struct StringInfo *s;
{
	*(s->UndoBuffer) = '\0';
	s->BufferPos = 0;
	s->DispPos = 0;
	s->UndoPos = 0;
	s->NumChars = strlen(s->Buffer);
}

/* Enable/disable speech capability, based on global 'do_speech'. */

SetSpeech()
{
	if (do_speech) {
		if (!SpeechOn()) {
			DateStamp(now);
			if (now->ds_Tick < 1500)
				Say("That feels good!  Thanks for turning me on!");
			else
				Say("Hi.  How may I help you?");
		}
	}
	else
		SpeechOff();
}

Speak(msg)
	char *msg;
{
	if (do_speech) Say(msg);
}

/* Perform a case-insensitive string compare.
 * Called with:
 *		s1, s2:		strings to be compared
 * Returns:
 *		comparison code:	0 	=> strings match
 *						   <0	=> s1 < s2
 *						   >0	=> s1 > s2
 */
int
strcmpc(s1, s2)
	register char *s1, *s2;
{
	int c1, c2, cd;

	do {
		c1 = tolower(*s1++);
		c2 = tolower(*s2++);
		if (cd = (c1 - c2)) break;
	} while (c1 && c2);

	return cd;
}

/* Type a message to the console and optionally "speak" it.
 * Called with:
 *		msg:		message string
 */
TypeAndSpeak(msg)
	char *msg;
{
	WriteConsole(msg);
	if (do_speech) Say(msg);
}

/* Handle IDCMP messages generated by user actions. */

User()
{
	ULONG class;				/* message class */
	USHORT code;				/* message code */
	USHORT gadgid;				/* gadget ID */
	APTR Iadr;					/* address field from message */
	struct IntuiMessage *msg;	/* Intuition message pointer */
	struct Window *msgwindow;	/* window message occurred in */
	USHORT quit = 0;
	SHORT x,y;					/* mouse x and y position */
	ULONG waitbits;

	waitbits = (1L << mywindow->UserPort->mp_SigBit) |
			   (1L << pathwindow->UserPort->mp_SigBit);
#ifdef DEBUG
	sprintf(debugmsg,"User: waitbits = %08lx\n", waitbits);
	DebugWrite(debugmsg);
#endif

	while (!quit) {
		ActivateWindow(mywindow);
		Wait(waitbits);
		while (!quit) {
			if (!(msg = (struct IntuiMessage *) 
				GetMsg(mywindow->UserPort)))
				if (!(msg = (struct IntuiMessage *)
					  GetMsg(pathwindow->UserPort)))
					break;

			class = msg->Class;
			code = msg->Code;
			Iadr = msg->IAddress;
			x = msg->MouseX;
			y = msg->MouseY;
			msgwindow = msg->IDCMPWindow;
			ReplyMsg(msg);		/* acknowledge the message */
#ifdef DEBUG
			sprintf(debugmsg,"Message class: 0x%lx, code: 0x%x\n",
				class, code);
#endif
			switch (class) {
			case CLOSEWINDOW:
				++quit;
				break;

			case GADGETUP:
			case GADGETDOWN:
				DoGadget(msgwindow, class, Iadr);
				break;

			case MENUPICK:
				quit = UserMenu(code);
				break;

			default:
				break;			/* ignore the rest */
			}					/* end switch(class) */
		}
	}
}

/* Handle a menu selection. 
 * Called with:
 *		xcode:		menu selection code
 * Returns:
 *		status code (1 => Quit was selected)
 */

int
UserMenu(xcode)
	USHORT xcode;
{
	USHORT code = xcode;
	struct MenuItem *item;
	USHORT itemnum,menunum;

	while (code != MENUNULL) {
		menunum = MENUNUM(code);
		itemnum = ITEMNUM(code);

#ifdef DEBUG
		sprintf(debugmsg,"menu = %d, item = %d\n",menunum,itemnum);
		DebugWrite(debugmsg);
#endif
		item = ItemAddress(&Titles[0], code);

		switch (menunum) {
		case MENU_PROJECT:			/* Project Menu */
			switch (itemnum) {
			case ITEM_BACKUP:		/* Backup */
				Backup();
				break;
			case ITEM_RESTORE:		/* Restore */
				Restore();
				break;
			case ITEM_ABOUT:		/* About */
				About();
				break;
			case ITEM_QUIT:			/* Quit */
				return 1;			
			default:
				DisplayBeep(NULL);
				break;
			}
			break;

		case MENU_FLAGS:			/* Flags Menu */
			switch (itemnum) {
			case ITEM_COMPRESS:		/* Compression */
				do_compress = IsChecked(item); 
				break;
			case ITEM_NOCOMPRESS:	/* No Compression */
				do_compress = !IsChecked(item);
				break;
			case ITEM_LIST:			/* Listing */
				do_listing = IsChecked(item);
				break;
			case ITEM_NOLIST:		/* No Listing */
				do_listing = !IsChecked(item);
				break;
			case ITEM_SPEECH:		/* Speech */
				do_speech = IsChecked(item);
				SetSpeech();
				break;
			case ITEM_NOSPEECH:		/* No Speech */
				do_speech = !IsChecked(item);
				SetSpeech();
				break;
			default:
				DisplayBeep(NULL);
				break;
			}
		}
#define EXTENDED_SELECT_WORKS
#ifdef EXTENDED_SELECT_WORKS
		code = item->NextSelect; 
		/* This next line is a kludge.  Testing has revealed that
		 * the NextSelect field is returning 0000x.  Why?
		 */
		if (!code) break;
#else
		break;
#endif
	}
	return 0;
}


/* Write a message to the console window.
 * Called with:
 *		msg:	message string
 */
WriteConsole(msg)
	char *msg;
{
	Write(console, msg, (long) strlen(msg));
}
