#include <fido.h>
#include <xfbuf.h>
#include <ascii.h>

#define same(s1,s2) (strcmp(s1,s2)==0)

char date[SS];		/* todays date */

int msgtotal;		/* total messages found */
int msghighest;		/* highest message found */
struct _msg msg;	/* current open message */
int msgfile;		/* its handle */
struct _fido fido;
char logname[SS];	/* log file name */

char *getmem();
long sizmem();

int caller_purge;	/* age to purge callers */
struct {
	char purge;	/* purge this many days old */
	char renum;	/* renumber this area */
} areas[MAXAREA];

struct _area msgarea;	/* current msg area */
struct _clr caller;	/* current open caller */

int age;		/* age to purge messages */
int log;		/* log file handle */

main(argc,argv)
int argc;
char **argv;
{
unsigned brkflg,i;
char fname[SS];

	printf("MSGMGR (4 June 90) Message Manager for ");
	copr();

	allmem();				/* free memory, */
	if (! fastfile(8)) {			/* start the file system */
		cprintf("Can't start the file system?\r\n");
		exit(1);
	}
	meminit();

	strcpy(fname,"MSGMGR.INI");		/* default file name, */
	if (argc > 1) strcpy(fname,argv[1]);	/* local name */

	i= open("fido.sys",0);			/* load system stuff */
	if (i == -1) {
		printf("The Fido system file FIDO.SYS is missing!\r\n");
		exit(1);
	}
	read(i,&fido,sizeof(fido));
	close(i);
	if (fido.fido_version != FIDOVER) {
		cprintf("Fido/FidoNet version doesn't match MsgMgr version!\r\n");
		exit(1);
	}
	caller_purge= 0;			/* dont purge callers by default */
	age= 90;				/* default age */
	for (i= 0; i < MAXAREA; i++) {
		areas[i].purge= 0;		/* dont purge messages */
		areas[i].renum= 0;		/* and renumber all areas, */
	}

	strcpy(logname,"fido.log");		/* default log file name */
	finit(fname);				/* look for a startup file */
	brkflg= _break(0,0);			/* get break, */
	_break(1,0);				/* turn it off */
	log= append(logname);			/* add to the log file, */
	if (log == -1) {
		stoupper(logname);
		cprintf("ERROR: cant open \"%s\"!\r\n",logname);
		_break(1,brkflg);
		exit(1);
	}
	gtod(date);				/* get todays date for purging */
	lprintf("-------------- MSGMGR started at %s, using file: %s \r\n",date,fname);

/* If we have nothing to do, say so and go home. */

	for (i= 0; i < MAXAREA; i++) {		/* search for something */
		if (areas[i].purge || areas[i].renum) break;
	}
	if ((i < MAXAREA) || caller_purge) {
		if (caller_purge) usrpurge(caller_purge); /* kill off some callers */

		for (i= 0; i < fido.marea_max; i++) {
			if (! getarea(i,&msgarea)) {
				cprintf("  There is no message area #%d!\r\n",i + 1);
				lprintf("! There is no message area #%d\r\n",i + 1);
				break;
			}
			if (areas[i].purge || areas[i].renum) {
				dsparea(&msgarea);		
			}
			if (areas[i].purge) msgpurge(i,areas[i].purge,0); /* purge first */
			if (iskey() == 3) break;		/* keyboard abort */
			if (areas[i].renum) msgrenum(i);	/* then renum */
			if (iskey() == 3) break;		/* keyboard abort */
		}

	} else {
		cprintf("  MsgMgr has nothing to do\r\n");
		lprintf("MSGMGR has nothing to do\r\n");
	}

	_break(1,brkflg);				/* restore break */

	gtod(date);
	lprintf("-------------- MSGMGR done at %s\r\n",date);
	close(log);					/* done with the log */
	exit(0);
}

/* Message file renumberer. Speeds up the system by renumbering
the messages so they are all sequential. */

msgrenum(area)
int area;
{
int *xl,xlsize;				/* reply translate table & its size */
int maxmsgs;				/* total msgs we can translate */
char *cp;
int i,n,f,lm;
unsigned r,bad;
char oldname[SS],newname[SS],buff[SS];
struct _clr caller;
struct _xfbuf xfbuf;
long p;

	xlsize= getbuf(400,10000,&xl);
	if (xlsize == 0) {				/* reply translate table */
		cprintf("  Not enough memory to RENUM?\r\n");
		lprintf("! Cant get memory to RENUM\r\n");
		return;
	}

	cprintf("  Renumbering Messages\r\n");
	lprintf(" Renum area #%d\r\n",area);

/* Make a table of 0's, which we then mark with 1's for message
numbers that exist. We use this table initially to find the first
non-sequential message number. */

	maxmsgs= xlsize / sizeof(int);			/* make it integers */
	for (n= 0; n < maxmsgs; n++) xl[n]= 0;		/* clear the table */

	msgtotal= 0;					/* total found, */
	msghighest= 0;

	makemname(buff,"????????.msg");			/* real pathname */
	xfbuf.s_attrib= 0; i= 0;
	while (_find(buff,i++,&xfbuf)) {
		n= atoi(xfbuf.name);			/* find its number, */
		if (n < maxmsgs) xl[n]= n;		/* mark as found, */
		++msgtotal;				/* remember the total, */
		if (n > msghighest) msghighest= n;	/* remember the highest */
	}

/* Now go look through the table for a hole. If no holes, nothing to do. */

	for (n= 1; n < maxmsgs; n++) {			/* search the table */
		if (! xl[n]) break;			/* for a missing msg */
	}

	if (n > msghighest) {
		cprintf("  Messages are already sequential\r\n");
		lprintf("Already sequential\r\n");
		rlsbuf(xl,xlsize);			/* free the buffer */
		return;
	}

/* Messages starting at (n) are non-sequential; we only have to start
renumbering from here. (n) is the first free message number; start
searching for an existing message and renumber it to that. */

	lm= n;						/* first new msg */

	i= n + 1;					/* next highest */
	while (i < maxmsgs) {
		if (! xl[i]) {				/* find one that exists */
			++i;
			continue;
		}
		sprintf(buff,"%d.msg",n);		/* free msg number */
		makemname(newname,buff);
		sprintf(buff,"%d.msg",i);		/* old msg number */
		makemname(oldname,buff);
		rename(oldname,newname);		/* rename it */

/* Now use this slot in the table to remember the old messages
new number so we can translate replies. */

		xl[i++]= n++;				/* new number */
	}

/* Now translate the reply numbers: xl[] is now a table of new message
numbers. */

	cprintf("  Translating Replies and See-Also's\r\n");
	makemname(oldname,"????????.msg");		/* real pathname */
	xfbuf.s_attrib= 0; i= 0;
	while (_find(oldname,i++,&xfbuf)) {
		n= atoi(xfbuf.name);			/* find a message, */
		if (n > maxmsgs) continue;		/* out of range */
		if (n < lm) continue;			/* no need to xlate */

		sprintf(buff,"%d.msg",n);		/* open it, */
		makemname(newname,buff);
		f= open(newname,2);
		if (f != -1) {				/* read the header, */
			read(f,&msg,sizeof(struct _msg));
			if ((msg.reply != xl[msg.reply]) || (msg.up != xl[msg.up])) {	/* translate replies */
				msg.reply= xl[msg.reply];
				msg.up= xl[msg.up];
				lseek(f,0L,0);		/* seek back, */
				write(f,&msg,sizeof(struct _msg));
			}
			close(f);
		}
	}
	cprintf("  Updating callers' last-read-message\r\n");

	strcpy(buff,fido.syspath);
	strcat(buff,"caller.sys");
	f= open(buff,2);
	if (f == -1) {
		cprintf("Cant find the caller file %s!\r\n",buff);
		lprintf("! Cant find %s\r\n",buff);
	}
	p= 0L;
	bad= r= 0;
	while (read(f,&caller,sizeof(struct _clr)) == sizeof(struct _clr)) {
		++r;
		if (caller.version != CLRVER) {
			cprintf(" ! Bad record #%u in %s -- use SYSOP.EXE's \"#\" command\r\n",r,buff);
			cprintf("   to locate it, and \"C\" to repair it\r\n");
			lprintf("! Bad record #%u in %s -- use SYSOP.EXE's \"#\" command\r\n",r,buff);
			lprintf("! to locate it, and \"C\" to repair it\r\n");
			if (++bad >= 10) {
				cprintf(" ! Too many contiguous bad CALLER.SYS records -- last-message update aborted\r\n");
				lprintf("! Too many contiguous bad CALLER.SYS records -- last-message update aborted\r\n");
				break;
			}
			continue;
		}
		bad= 0;

/* If the message area being renumbered is in the caller record, translate
the "highest msg read" to the new message number; if that message was
deleted it will translate to zero; find the closest message we can. */

		for (i= 0; i < MAXLREAD; i++) {
			if (caller.lastmsg[i].area == area) {
				n= caller.lastmsg[i].msg; 
				while (xl[n] == 0) {		/* if msg gone, */
					if (--n <= 0) break;	/* find next lowest */
				}
				if (n <= 0) break;		/* nothing set */
				caller.lastmsg[i].msg= xl[n];	/* new msg number */
				lseek(f,p,0);
				write(f,&caller,sizeof(struct _clr));
				break;
			}
		}
		p += (long) sizeof(struct _clr);
	}
	close(f);
	xlt_file("HW.DAT",xl,maxmsgs);		/* do DCM high water marker */
	xlt_file("LASTREAD",xl,maxmsgs);	/* do SEADOG last-read marker */
	rlsbuf(xl,xlsize);			/* free the buffer */
}

/* Attempt to translate DCMs HW.DAT file; it is a two byte file containing
a message number; simply translate it. */

xlt_file(fn,xl,maxmsgs)
int *xl;		/* message number translate table */
int maxmsgs;		/* highest input message number */
{
char buff[SS];
int o,f;

unsigned n;

	makemname(buff,fn);			/* blindly try it */
	f= open(buff,2);
	if (f == -1) return;			/* doesnt exist */
	if (read(f,&n,sizeof(n)) != sizeof(n)) n= 0;/* if bad reset it */
	o= n;					/* save for report */
	if (n > maxmsgs) n= maxmsgs;		/* bound it */

/* Translate the message number; if the table entry is zero, then that
message doesn't exist; use the next-lowest message number, or zero. */

	while (n && (xl[n] == 0)) --n;		/* til table entry, or n == 0 */
	if (n) if (xl[n]) n= xl[n];		/* if found, use it */

	cprintf("  \"%s\" translated from #%d to #%d\r\n",fn,o,n);
	lprintf("%s from %d to %d\r\n",fn,o,n);
	lseek(f,0L,0);				/* rewrite those two bytes */
	write(f,&n,sizeof(n));
	close(f);
}
		
/* Purge old callers. Works by copying to a temp file, except the
deleted ones, then renaming. */

usrpurge(age)
int age;
{
int i,u,t,o,n;
unsigned r,bad;		/* record number, bad in a row */

#define SIZE (sizeof(struct _clr))

	cprintf("  Purging callers %d days old\r\n",age);
	lprintf(" Purge callers %d days old\r\n",age);

	u= open("caller.sys",0);
	if (u == -1) {
		lprintf("! Cant find CALLER.SYS\r\n");
		return;
	}
	t= creat("caller$$.$$$",2);
	if (t == -1) {
		lprintf("! Cant create temp file CALLER$$.$$$\r\n");
		close(u);
		return;
	}
	o= creat("caller.old",2);
	if (o == -1) {
		lprintf("! cant create old caller file CALLER.OLD\r\n");
		close(u);
		close(t);
		return;
	}
	bad= r= n= 0;
	while (read(u,&caller,SIZE) == SIZE) {
		++r;
		if (caller.version != CLRVER) {
			cprintf(" ! Bad record #%u in CALLER.SYS -- use SYSOP.EXE's \"#\" command\r\n",r);
			cprintf("   to locate it, and \"C\" to repair it\r\n");
			lprintf("! Bad record #%u in CALLER.SYS -- use SYSOP.EXE's \"#\" command\r\n",r);
			lprintf("! to locate it, and \"C\" to repair it\r\n");
			if (++bad >= 10) {
				cprintf(" ! Too many contiguous bad CALLER.SYS records -- PURGE aborted\r\n");
				lprintf(" ! Too many contiguous bad CALLER.SYS records -- PURGE aborted\r\n");
				n= -1;
				break;
			}

		} else if ((days(caller.date,date) < age) || /* if new, */
		    (caller.stuff & (1L << CLR_KEPS)) || /* marked KEEP, */
		    caller.credit ||			/* has credit, */
		    (uval(CLR_PRV,CLR_PRVS) == SYSOP)) { /* or is SYSOP */
			bad= 0;
			if (write(t,&caller,SIZE) != SIZE) { /* keep 'em */
				lprintf("! Purge callers: Disk full!\r\n");
				n= -1;			/* flag the error */
				break;
			}

		} else {				/* purge 'em */
			bad= 0;
			if (write(o,&caller,SIZE) != SIZE) { /* write to old file */
				lprintf("! Purge callers: Disk full!\r\n");
				n= -1;
				break;
			}
			++n;				/* count another purged */
		}
	}
	close(u);
	close(t);
	close(o);
	if (n < 0) return;		/* if write error */

	delete("caller.bak");
	rename("caller.sys","caller.bak");
	rename("caller$$.$$$","caller.sys");
	lprintf(" Purged %d callers\r\n",n);
}
/* Return the value from the caller bit record. */

uval(mask,shift)
int mask,shift;
{
int n;

	n= (caller.stuff >> shift) & mask;
	return(n);
}

/* Set a field in the caller bit record. */

setuval(val,mask,shift)
int val,mask,shift;
{
long n;

	n= mask; n <<= shift;			/* we NEED long arith */
	caller.stuff &= ~n;			/* remove old value */

	n= val & mask; n <<= shift;		/* set it THEN shift it */
	caller.stuff |= n;			/* add in new value */
}
/* Purge messages so many days old. */

msgpurge(area,daysold,killrecvd)
int area;			/* which area */
int daysold;			/* message age */
int killrecvd;			/* kill if PRIVATE & RECV'D */
{
int count,kill_it;
int i;
struct _xfbuf xfbuf;
char *dp,*cp,name[SS],buff[SS];

	cprintf("  Purging messages %d days old\r\n",daysold);
	lprintf(" purge msgs %d days old\r\n",daysold);

	makemname(name,"*.msg");
	count= 0;
	xfbuf.s_attrib= 0; i= 0;
	while (_find(name,i++,&xfbuf)) {		/* find a msg file */

		makemname(buff,xfbuf.name);		/* assemble its name */
		msgfile= open(buff,0);			/* read its header */
		if (msgfile == -1) continue;
		read(msgfile,&msg,sizeof(struct _msg));	/* load the header, */
		close(msgfile);

		kill_it= killrecvd && (msg.attr & MSGREAD) && (msg.attr & MSGPRIVATE);
		kill_it |= daysold && ((days(msg.date,date) > daysold));
		if (kill_it) {
			delete(buff);
			++count;
		}
	}
	cprintf("  Purged %d messages\r\n",count);
	lprintf(" Purged %d msgs\r\n",count);
}

/* Get message area #n; return 0 if not set, illegal number
or other error. */

getarea(n,a)
unsigned n;		/* area number */
struct _area *a;	/* struct to load */
{
int off,f,i;

	if (n < 0) return(0);				/* no such area */
	if (n >= fido.marea_max) return(0);		/* msg area max */

	f= open("fido.sys",0);
	if (f == -1) return(0);				/* no system file! */

	off= sizeof(struct _fido);			/* offset to beg msg areas */
	off += n * sizeof(struct _area);		/* offset plus area # */
	lseek(f,(0L + off),0);				/* seek there, */
	i= read(f,a,sizeof(struct _area));		/* read some, */
	close(f);					/* immediately close it */

	if (i != sizeof(struct _area)) return(0);	/* check read error */
	a-> number= n;					/* set the path number */
	return(*a-> path != NUL);			/* invalid if no path set */
}
/* Display the area contents. */

dsparea(area)
struct _area *area;
{
	cprintf("\r\n%d: ",area-> number + 1);
	cputs(area-> desc);
	cputs("\r\n");
}

#define ERROR -1
#define IDLE -2
#define IGNORE -3

#define AGE 10		/* AGE <n> 2 states, */
#define AGE2 11		/* keyword, number */

#define PURGE 20	/* PURGE areas ... */
#define PURGE2 21	/* keyword, numbers */

#define RENUM 30	/* RENUM areas ... */
#define RENUM2 31	/* areas ... */

#define LOG 40		/* LOG <filename> */
#define LOG2 41		/* filename */

static struct {
	char word[20];
	int state;

} rct[] = {
	"renum",RENUM,
	"purge",PURGE,
	"age",AGE,
	"logfile",LOG,

	"",ERROR
};

finit(fn)
char *fn;
{
int i,f,lineno,value;
char line[200],atom[SS],*cp;
FLAG callers,all,not;
int state;

	cprintf("  Reading startup file %s\r\n",fn);
	f= open(fn,0);
	if (f == -1) {
		cprintf("  It's not there!\r\n");
		return(0);
	}
	lineno= 0;				/* for error reports */
	callers= all= not= state= 0;		/* reset all */

	while (rline(f,line,sizeof(line))) {
		++lineno;
		for (cp= line; *cp; ++cp) {	/* delete comments */
			if (*cp == ';') {	/* by marking */
				*cp= NUL;	/* as the end of the line */
				break;
			}
		}

		cp= skip_delim(line);
		while (num_args(cp)) {
			cpyatm(atom,cp);	/* this atom, */
			stolower(atom);		/* make it pretty, */
			cp= next_arg(cp);	/* for next time through */

/* Search for the keyword in the table; if not found, leave as state
ERROR. Digits are always OK, so no error if a digit. (Even if there is
no state to accept them.) */

			if (same(atom,"all")) all= 1;
			else if (same(atom,"none")) {not= 1; all= 1;}
			else if (same(atom,"not")) not= 1;
			else if (same(atom,"callers")) callers= 1;

			else if (isdigit(*atom)) value= atoi(atom);
			else switch (state) {
				case LOG2: break;	/* ignore; arg is filename */

				default:
					for (i= 0; *rct[i].word; i++) {
						if (same(atom,rct[i].word)) break;
					}
					if (*rct[i].word) {
						if (rct[i].state != IGNORE)
							state= rct[i].state;

					} else state= ERROR;
					break;
			}

/* We switch states when we find a keyword, but only start executing states
until the thing following a keyword. Ie. after SEND-TO we have to wait
for a number. */

			switch (state) {

				case IDLE: break;
				case IGNORE: break;

				case LOG: state= LOG2; break;
				case LOG2: cpyarg(logname,atom); state= IDLE; break;

				case ERROR: 
					cprintf("   Unknown word '%s'\r\n",atom);
					break;

				case AGE: state= AGE2; break;
				case AGE2: 
					if ((value > 1) && (value < 1000)) age= value;
					else {
						cprintf("AGE must be 2 to 999 days\r\n");
						state= ERROR;
					}
					callers= all= not= 0;
					break;

				case PURGE: state= PURGE2; break;
				case PURGE2: 

					if (callers) {
						if (! not) caller_purge= age;

					} else if (all) {
						for (i= 0; i <= fido.marea_max; i++) {
							areas[i].purge= (not ? 0 : age);
						}

					} else if (value <= fido.marea_max) {
						areas[value - 1].purge= (not ? 0 : age);

					} else cprintf("There is no area %d to PURGE!\r\n",value);
					callers= all= not= 0;
					break;

				case RENUM: state= RENUM2; break;
				case RENUM2: 
					if (all) {
						for (i= 0; i <= fido.marea_max; i++)
							areas[i].renum= ! not;

					} else if (value <= fido.marea_max) {
						areas[value - 1].renum= ! not;

					} else cprintf("There is no area %d to RENUM!\r\n",value);
					callers= all= not= 0;
					break;
			}
		}
	}
	close(f);
}
/* Write out the open message. */

wrtmsg() {
	lseek(msgfile,0L,0);			/* seek to start, */
	write(msgfile,&msg,sizeof(struct _msg)); /* write it out, */
	close(msgfile);
}
/* Assemble a full msg filename */

makemname(d,p)
char *d,*p;
{
	strcpy(d,msgarea.path);		/* put in the path, */
	strcat(d,p);			/* add the filename */
}
/* Formatted print to the log file. If the log fills up, mark the handle
as -1, stop writing to it. */

lprintf(f)
char *f;
{
char *cp,buf[500];

	if (log == -1) return;
	_spr(buf,&f);

	if (write(log,buf,strlen(buf)) != strlen(buf)) {
		cprintf("LOG FILE: DISK FULL!!\r\n");
		log= -1;
	}
}
/* Open a file for appending, creating it if necessary. Return the
open handle, or -1 if error. */

append(s)
char *s;
{
int h;

	h= open(s,2);				/* open or create the */
	if (h == -1) h= creat(s,2);		/* file, if opened OK */
	else lseek(h,0L,2);			/* seek to the end */
	return(h);				/* handle or error */
}
/* Dummy */

clprintf() {}

/* Error printer */

error(s)
char *s;
{
	printf("!!! %s\r\n",s);
}
