/* Recent.c */

#include "Sys.h"

#include <ctype.h>

/* TO-DO: If I decide to implement strstr matching, watch out for
 * local domain hosts, with simple names.
 */

#include "Util.h"
#include "Recent.h"
#include "FTP.h"

/* We keep an array of structures of all the sites we know about in
 * a contigious block of memory.
 */
RemoteSiteInfoPtr gHosts = (RemoteSiteInfoPtr) 0;

/* But we also keep an array of pointers to the structures, so we can
 * use that for sorting purposes.  We don't want to have to shuffle
 * large structures around when sorting.  As the name hints, we have
 * this array sorted by nickname, for quick searching.
 */
RemoteSiteInfoPtrList gHostNickNames = (RemoteSiteInfoPtrList) 0;

/* This is the number of elements in both gHosts and gHostNickNames. */
int gNumRecents = 0;

/* We don't want the other portions of the program overwriting the
 * current entry in the gHosts list, because we may want to save our
 * changes under a new entry.  So if the host was in our list, we make
 * a copy of the data, and let them write into that.  Then we can write
 * the changed structure under a new entry, or overwrite the old one.
 * This also works for entirely new entries.  We just give the caller
 * this, initialized to the default values.
 */
RemoteSiteInfo gRmtInfo = { NULL, NULL, 0, "", "" };

/* Used to tell if we got the information from the host information list,
 * or we were using a new entry.
 */
int gRmtInfoIsNew;

/* We use this outside of this module to tell if we should actually
 * save the information collected.  We don't want to save it if the
 * stuff wasn't really valid, so we won't save unless you logged in
 * successfully.
 */
int gWantRmtInfoSaved;

/* Used to tell if the host file needs to be written back out.
 * If we haven't changed anything, then don't waste the time to
 * write the file.
 */
int gHostsModified = 0;

/* These are the first and last nodes in the linked-list of remote
 * site information structures.
 */
RemoteSiteInfoPtr gFirstRsi = NULL, gLastRsi = NULL;

/* If greater than zero, we will only save the most recent sites, up
 * to this number.
 */
int gMaxRecents = kNoRecentLimit;

extern string gEmailAddress, gAnonPassword;
extern int gPreferredDataPortMode;

static
int RecentSortProc(const RemoteSiteInfoPtr *a, const RemoteSiteInfoPtr *b)
{
	return (ISTRCMP((**a).nickName, (**b).nickName));	
}	/* RecentSortProc */



static
int RecentSortTimeProc(const RemoteSiteInfoPtr *a, const RemoteSiteInfoPtr *b)
{
	return ((**b).lastCall - (**a).lastCall);	
}	/* RecentSortTimeProc */



static
int RecentSearchProc(char *key, const RemoteSiteInfoPtr *b)
{
	return (ISTRCMP(key, (**b).nickName));	
}	/* RecentSearchProc */





void SortNickNames(void)
{
	int i;
	RemoteSiteInfoPtr p;

	gHostNickNames = (RemoteSiteInfoPtrList) Realloc(
		gHostNickNames,		/* Okay if NULL. */
		sizeof(RemoteSiteInfoPtr) * (gNumRecents + 1)
	);
	if (gHostNickNames == (RemoteSiteInfoPtrList) 0)
		OutOfMemory();
	
	for (p = gFirstRsi, i=0; p != NULL; i++, p = p->next) {
		gHostNickNames[i] = p;
	}
	gHostNickNames[gNumRecents] = NULL;

	QSORT(gHostNickNames,
		gNumRecents, sizeof(RemoteSiteInfoPtr), RecentSortProc);
	
	for (i=0; i<gNumRecents; i++)
		gHostNickNames[i]->index = i;
}	/* SortNickNames */




void UpdateRemoteSiteInfoPtr(RemoteSiteInfoPtr dst, RemoteSiteInfoPtr src)
{
	RemoteSiteInfoPtr next, prev;
	int idx;
	
	/* Need to preserve dst's links, but copy all of src's stuff. */
	next = dst->next;
	prev = dst->prev;
	idx = dst->index;
	*dst = *src;
	dst->next = next;
	dst->prev = prev;
	dst->index = idx;
}	/* UpdateRemoteSiteInfoPtr */





RemoteSiteInfoPtr AddRemoteSiteInfoPtr(RemoteSiteInfoPtr buf)
{
	RemoteSiteInfoPtr newRsip;

	newRsip = (RemoteSiteInfoPtr) malloc(sizeof(RemoteSiteInfo));
	if (newRsip != NULL) {
		*newRsip = *buf;
		newRsip->next = NULL;
		if (gFirstRsi == NULL) {
			gFirstRsi = gLastRsi = newRsip;
			newRsip->prev = NULL;
		} else {
			newRsip->prev = gLastRsi;
			gLastRsi->next = newRsip;
			gLastRsi = newRsip;
		}
		++gNumRecents;
		/* Just need to know if we should write out the host file later. */
		gHostsModified++;
	} else {
		OutOfMemory();
	}
	return newRsip;
}	/* AddRemoteSiteInfoPtr */





RemoteSiteInfoPtr RemoveRemoteSiteInfoPtr(RemoteSiteInfoPtr killMe)
{
	RemoteSiteInfoPtr nextRsi, prevRsi;
	
	nextRsi = killMe->next;	
	prevRsi = killMe->prev;	
	
	if (gFirstRsi == killMe)
		gFirstRsi = nextRsi;
	if (gLastRsi == killMe)
		gLastRsi = prevRsi;

	if (nextRsi != NULL)
		nextRsi->prev = prevRsi;
	if (prevRsi != NULL)
		prevRsi->next = nextRsi;

	PTRZERO(killMe, sizeof(RemoteSiteInfo));
	free(killMe);
	--gNumRecents;
	++gHostsModified;
	return (nextRsi);
}	/* RemoveRemoteSiteInfoPtr */





void MakeNickNameUnique(char *dst, size_t siz)
{
	int i;
	string s2, s3;
	RemoteSiteInfoPtr *rsipp;
	char *cp;

	/* Make sure we can concat 3 more characters if necessary. */
	Strncpy(s2, dst, siz - 3);
	for (cp = s2 + strlen(s2) - 1; cp > s2; ) {
		if (isdigit(*cp))
			*cp-- = '\0';
		else
			break;
	}

	/* Make a copy of the original. */
	STRNCPY(s3, dst);
	
	for (i=1; i<=999; i++) {
		if (i > 1)
			sprintf(dst, "%s%d", s2, i);
		else
			Strncpy(dst, s3, siz);
	
		/* See if there is already a nickname by this name. */
		if (gNumRecents == 0)
			break;
		rsipp = (RemoteSiteInfoPtr *) BSEARCH(dst, gHostNickNames, gNumRecents,
			sizeof(RemoteSiteInfoPtr), RecentSearchProc);
		if (rsipp == NULL)
			break;
	}
}	/* MakeNickNameUnique */




void MakeUpANickName(char *dst, size_t siz, char *src)
{
	string str;
	char *token;
	char *cp;

	STRNCPY(str, src);
	
	/* Pick the first "significant" part of the hostname.  Usually
	 * this is the first word in the name, but if it's something like
	 * ftp.unl.edu, we would want to choose "unl" and not "ftp."
	 */
	token = str;
	if ((token = strtok(token, ".")) == NULL)
		token = "misc";
	else if (ISTREQ(token, "ftp")) {
		if ((token = strtok(NULL, ".")) == NULL)
			token = "misc";
	}
	for (cp = token; ; cp++) {
		if (*cp == '\0') {
			/* Token was all digits, like an IP address perhaps. */
			token = "misc";
		}
		if (!isdigit(*cp))
			break;
	}
	Strncpy(dst, token, siz);
	MakeNickNameUnique(dst, siz);
}	/* MakeUpANickName */




void SetRemoteInfoDefaults(RemoteSiteInfoPtr rsip)
{
	PTRZERO(rsip, sizeof(RemoteSiteInfo));

	rsip->xferType = 'I';
	rsip->port = kPortUnset;
	rsip->hasSIZE = 1;
	rsip->hasMDTM = 1;
	if (gPreferredDataPortMode >= kPassiveMode) {
		/* Assume we have it until proven otherwise. */
		rsip->hasPASV = 1;
	} else {
		/* If default is PORT, then make the user explicitly set this. */
		rsip->hasPASV = 0;
	}	
	rsip->isUnix = 1;
	rsip->lastCall = (time_t) 0;
}	/* SetRemoteInfoDefaults */



void SetNewRemoteInfoDefaults(RemoteSiteInfoPtr rsip)
{
	/* Return a pointer to a new entry, initialized to
	 * all the defaults, except for name and nickname.
	 */
	SetRemoteInfoDefaults(rsip);
	STRNCPY(rsip->name, "foobar.snafu.gov");
	STRNCPY(rsip->nickName, "NEW");

	/* That will make a unique "NEW" nickname. */
	MakeNickNameUnique(rsip->nickName, sizeof(rsip->nickName));
}	/* SetNewRemoteInfoDefaults */




int GetRemoteInfo(char *host, size_t siz)
{
	RemoteSiteInfoPtr *rsipp;
	int i;
	size_t len;
	
	if (gNumRecents == 0)
		rsipp = NULL;
	else {
		rsipp = (RemoteSiteInfoPtr *)
			BSEARCH(host, gHostNickNames, gNumRecents,
				sizeof(RemoteSiteInfoPtr), RecentSearchProc);
		if (rsipp == NULL) {
			/* No exact match, but the user doesn't have to type the
			 * whole nickname, just the first few letters of it.
			 */
			/* This could probably be done in a bsearch proc too... */
			len = strlen(host);
			for (i=0; i<gNumRecents; i++) {
				if (ISTRNEQ(gHostNickNames[i]->nickName, host, len)) {
					rsipp = &gHostNickNames[i];
					break;
				}
			}
		}
		if ((rsipp == NULL) && (strchr(host, '.') != NULL)) {
			/* If thing we were given looks like a full hostname (has at
			 * least one period), see if we have an exact match on the
			 * hostname.
			 *
			 * This is actually not recommended -- you should try to use
			 * the nicknames only since they are unique.  We can have more
			 * than one entry for the same hostname!
			 */
			for (i=0; i<gNumRecents; i++) {
				if (ISTREQ(gHostNickNames[i]->name, host)) {
					rsipp = &gHostNickNames[i];
					break;
				}
			}
		}
	}

	gWantRmtInfoSaved = 0;
	if (rsipp != NULL) {
		gRmtInfo = **rsipp;
		
		/* So we know that this isn't in the list, but just a copy
		 * of someone else's data.
		 */
		gRmtInfo.next = gRmtInfo.prev = NULL;
		
		gRmtInfoIsNew = 0;
		/* gHost needs to be set here, since the caller wasn't using
		 * a real host name.
		 */
		Strncpy(host, gRmtInfo.name, siz);
		return (1);
	}
	
	SetNewRemoteInfoDefaults(&gRmtInfo);	
	STRNCPY(gRmtInfo.name, host);
	MakeUpANickName(gRmtInfo.nickName, sizeof(gRmtInfo.nickName), host);
	
	gRmtInfoIsNew = 1;
	return (0);
}	/* GetRemoteInfo */




int ParseHostLine(char *line, RemoteSiteInfoPtr rsip)
{
	string token;
	char *s, *d;
	long L;
	int i;
	int result;

	SetRemoteInfoDefaults(rsip);
	s = line;
	result = -1;
	for (i=0; ; i++) {
		if (*s == '\0')
			break;
		/* Some tokens may need to have a comma in them.  Since this is a
		 * field delimiter, these fields use \, to represent a comma, and
		 * \\ for a backslash.  This chunk gets the next token, paying
		 * attention to the escaped stuff.
		 */
		for (d = token; *s != '\0'; ) {
			if ((*s == '\\') && (s[1] != '\0')) {
				*d++ = s[1];
				s += 2;
			} else if (*s == ',') {
				++s;
				break;
			} else {
				*d++ = *s++;
			}
		}
		*d = '\0';
		switch(i) {
			case 0: (void) STRNCPY(rsip->nickName, token); break;
			case 1: (void) STRNCPY(rsip->name, token); break;
			case 2: (void) STRNCPY(rsip->user, token); break;
			case 3: (void) STRNCPY(rsip->pass, token); break;
			case 4: (void) STRNCPY(rsip->acct, token); break;
			case 5: (void) STRNCPY(rsip->dir, token);
					result = 0;		/* Good enough to have these fields. */
					break;
			case 6: rsip->xferType = token[0]; break;
			case 7:
				/* Most of the time, we won't have a port. */
				if (token[0] == '\0')
					rsip->port = (unsigned int) kDefaultFTPPort;
				else
					rsip->port = (unsigned int) atoi(token);
				break;
			case 8:
				sscanf(token, "%lx", &L);
				rsip->lastCall = (time_t) L;
				break;
			case 9: rsip->hasSIZE = atoi(token); break;
			case 10: rsip->hasMDTM = atoi(token); break;
			case 11: rsip->hasPASV = atoi(token); break;
			case 12: rsip->isUnix = atoi(token);
					result = 3;		/* Version 3 had all fields to here. */
					break;
			case 13: (void) STRNCPY(rsip->lastIP, token); break;
			case 14: (void) STRNCPY(rsip->comment, token); break;
			case 15: sscanf(token, "%ld", &rsip->xferKbytes); break;
			case 16: sscanf(token, "%ld", &rsip->xferHSeconds);
					result = 4;		/* Version 4 had all fields up to here. */
					break;
			case 17: rsip->nCalls = atoi(token);
					result = 5;		/* Version 5 has all fields to here. */
					break;
			default:
					result = 99;	/* Version >4 ? */
					goto done;
		}
	}
done:
	return (result);
}	/* ParseHostLine */




void ReadRemoteInfoFile(void)
{
	string pathName;
	string path2;
	FILE *fp;
	longstring line;
	int version;
	RemoteSiteInfo newRsi;

	OurDirectoryPath(pathName, sizeof(pathName), kRecentFileName);
	fp = fopen(pathName, "r");
	if (fp == NULL)
		return;		/* Okay to not have one yet. */
	
	if (FGets(line, sizeof(line), fp) == NULL)
		goto badFmt;
	
	/* Sample line we're looking for:
	 * "NcFTP hostfile version: 2"
	 */
	version = -1;
	(void) sscanf(line, "%*s %*s %*s %d", &version);
	if (version < kRecentMinVersion) {
		if (version < 0)
			goto badFmt;
		STRNCPY(path2, pathName);
		sprintf(line, ".v%d", version);
		STRNCAT(path2, line);
		(void) rename(pathName, path2);
		Error(kDontPerror, "%s: old version.\n", pathName);
		fclose(fp);
		return;
	}
		
	if (FGets(line, sizeof(line), fp) == NULL)
		goto badFmt;
	
	/* Sample line we're looking for:
	 * "Number of entries: 28"
	 */
	gNumRecents = -1;
	
	/* At the moment, we don't really care about the number stored in the
	 * file.  It's there for future use.
	 */
	(void) sscanf(line, "%*s %*s %*s %d", &gNumRecents);
	if (gNumRecents < 0)
		goto badFmt;
	
	gHosts = (RemoteSiteInfoPtr) 0;
	gHostNickNames = (RemoteSiteInfoPtrList) 0;
	gNumRecents = 0;
	
	while (FGets(line, sizeof(line), fp) != NULL) {
		if (ParseHostLine(line, &newRsi) >= 0) {
			AddRemoteSiteInfoPtr(&newRsi);
		}
	}
	fclose(fp);

	SortNickNames();
	DebugMsg("Read %d entries from %s.\n", gNumRecents, pathName);
	return;
	
badFmt:
	Error(kDontPerror, "%s: invalid format.\n", pathName);
	fclose(fp);
}	/* ReadRemoteInfoFile */




RemoteSiteInfoPtr DuplicateRemoteInfo(RemoteSiteInfoPtr origrsip)
{
	RemoteSiteInfo newRsi;
	RemoteSiteInfoPtr newRsip;
	string str;

	STRNCPY(str, origrsip->nickName);
	MakeNickNameUnique(str, sizeof(origrsip->nickName));

	newRsi = *origrsip;
	STRNCPY(newRsi.nickName, str);
	newRsip = AddRemoteSiteInfoPtr(&newRsi);

	/* Have to re-sort now so our bsearches will work. */
	SortNickNames();
	return (newRsip);
}	/* DuplicateRemoteInfo */




void DeleteRemoteInfo(RemoteSiteInfoPtr rsip)
{
	if (gNumRecents < 1)
		return;
	
	RemoveRemoteSiteInfoPtr(rsip);
	SortNickNames();
}	/* DuplicateRemoteInfo */




void SaveRemoteInfo(char *asNick)
{
	RemoteSiteInfoPtr *rsipp;

	if (gRmtInfoIsNew) {
		(void) AddRemoteSiteInfoPtr(&gRmtInfo);
		PrintF("Saving new entry nicknamed \"%s\" in your host file.\n",
			gRmtInfo.nickName);

		/* Have to re-sort now so our bsearches will work. */
		SortNickNames();
	} else {
		/* We were working with an existing entry.
		 * If the nickname given to us as the parameter is different
		 * from the existing nickName, then we're supposed to save
		 * this as a new entry.
		 */
		if ((asNick == NULL) || ISTREQ(asNick, gRmtInfo.nickName)) {
			/* Save over old entry. */
			rsipp = (RemoteSiteInfoPtr *) BSEARCH(gRmtInfo.nickName,
				gHostNickNames, gNumRecents,
				sizeof(RemoteSiteInfoPtr), RecentSearchProc);
			/* This had better be in there, since we did this before
			 * and it was in there.
			 */
			if (rsipp == NULL) {
				Error(kDontPerror,
				"Programmer's error: couldn't re-find host info entry.\n");
				return;
			}
			/* Copy over the old stuff. */
			UpdateRemoteSiteInfoPtr(*rsipp, &gRmtInfo);

			/* Just need to know if we should write out the host file later. */
			gHostsModified++;
		} else {
			/* Add a new entry. */
			STRNCPY(gRmtInfo.nickName, asNick);
			MakeNickNameUnique(gRmtInfo.nickName, sizeof(gRmtInfo.nickName));
			(void) AddRemoteSiteInfoPtr(&gRmtInfo);

			/* Have to re-sort now so our bsearches will work. */
			SortNickNames();
		}
	}

	gRmtInfoIsNew = 0;
}	/* SaveRemoteInfo */



static
void EscapeString(char *d, char *s)
{
	if (s != NULL) {
		while (*s != '\0') {
			if (*s == ',' || *s == '\\')
				*d++ = '\\';
			*d++ = *s++;
		}
	}
	*d = '\0';
}	/* EscapeString */




void WriteRemoteInfoFile(void)
{
	string pathName;
	string bupPathName;
	longstring escapedStr;
	FILE *fp;
	char portStr[16];
	int i;
	int nPasswds;
	RemoteSiteInfoPtr rsip;

	if (!gHostsModified)
		return;

	OurDirectoryPath(pathName, sizeof(pathName), kRecentFileName);

	if ((gMaxRecents != kNoRecentLimit) && (gNumRecents > gMaxRecents)) {
		DebugMsg("Purging %d old remote sites from %s.\n",
			gNumRecents - gMaxRecents,
			pathName
		);

		/* Sort sites by last time we called.  We want the older sites to
		 * float to the bottom.
		 */
		QSORT(gHostNickNames,
			gNumRecents, sizeof(RemoteSiteInfoPtr), RecentSortTimeProc);

		gNumRecents = gMaxRecents;
	}
	
	/* See if we can move the existing file to a new name, in case
	 * something happens while we write this out.  Host files are
	 * valuable enough that people would be pissed off if their
	 * host file got nuked.
	 */
	OurDirectoryPath(bupPathName, sizeof(bupPathName), kRecentBupFileName);
	(void) UNLINK(bupPathName);
	(void) rename(pathName, bupPathName);
	
	fp = fopen(pathName, "w");
	if (fp == NULL)
		goto err;
	
	if (fprintf(fp, "NcFTP hostfile version: %d\nNumber of entries: %d\n",
		kRecentVersion,
		gNumRecents
	) < 0)
		goto err;
	if (fflush(fp) < 0)
		goto err;

	for (i=0, nPasswds=0; i<gNumRecents; i++) {
		*portStr = '\0';
		rsip = gHostNickNames[i];
		if (rsip->port != kDefaultFTPPort)
			sprintf(portStr, "%u", rsip->port);
		if ((rsip->pass[0] != '\0')
			&& (!STREQ(rsip->pass, gEmailAddress))
			&& (!STREQ(rsip->pass, gAnonPassword)))
			nPasswds++;

		/* Insert the quote character '\' for strings that can have
		 * commas or backslashes in them.
		 */
		EscapeString(escapedStr, rsip->pass);
		if (fprintf(fp, "%s,%s,%s,%s,%s,",
			rsip->nickName,
			rsip->name,
			rsip->user,
			escapedStr,
			rsip->acct
		) < 0)
			goto err;

		EscapeString(escapedStr, rsip->dir);
		if (fprintf(fp, "%s,%c,%s,%lx,%d,%d,%d,%d,",
			escapedStr,
			rsip->xferType,
			portStr,
			(unsigned long) rsip->lastCall,
			rsip->hasSIZE,
			rsip->hasMDTM,
			rsip->hasPASV,
			rsip->isUnix
		) < 0)
			goto err;

		EscapeString(escapedStr, rsip->comment);
		if (fprintf(fp, "%s,%s,%ld,%ld,%d\n",
			rsip->lastIP,
			escapedStr,
			rsip->xferKbytes,
			rsip->xferHSeconds,
			rsip->nCalls
		) < 0)
			goto err;
	}
	if (fclose(fp) < 0) {
		fp = NULL;
		goto err;
	}
	(void) UNLINK(bupPathName);
	if (nPasswds > 0) {
		/* Set permissions so other users can't see the passwords.
		 * Of course this isn't really secure, which is why the program
		 * won't save passwords entered at the password prompt.  You must
		 * explicitly set them from the host editor.
		 */
		(void) chmod(pathName, 0600);	/* Set it to -rw------- */
	}
	return;

err:
	if (access(bupPathName, F_OK) < 0) {
		Error(kDoPerror, "Could not write to %s.\n", pathName);
	} else {
		/* Move backup file back to the original. */
		rename(bupPathName, pathName);
		Error(kDoPerror, "Could not update %s.\n", pathName);
	}
	if (fp != NULL)
		fclose(fp);
}	/* WriteRemoteInfoFile */
