/*
 *   Copyright 1992, 1993, 1994 John Melton (G0ORX/N6LYT)
 *              All Rights Reserved
 *
 *   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 1, 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

/*
	xpb.c

	eXperimental Pacsat Broadcast Protocal Receiver
	 -           -      -

	This software was written to provide a receive only capability
	for collecting files and directories from satellites running
	the Pacsat Protocols running on the Linux Operating System.

	This program has been run using the 1.0 version of the
	Linux kernel with the patches from Alan Cox to provide AX.25
	encapsulation of SLIP.

	The TNC must be setup for KISS.

	John Melton
	G0ORX, N6LYT

	4 Charlwoods Close
	Copthorne
	West Sussex
	RH10 3QZ
	England

	INTERNET:	g0orx@amsat.org
			n6lyt@amsat.org
			john@images.demon.co.uk
			J.D.Melton@slh0613.icl.wins.co.uk

	History:
	-------

	0.1	Initial version - no GUI interface.		G0ORX
	0.2	Added X-Windows (OpenView) interface.		G0ORX
	0.3	Added transmit capability for file and hole fill requests.
	0.4	Added ability to receive fragmented file headers for directory
		broadcasts,  and assemble them together (most headers are
		less than the 244 byte packet limit, but some people have
		set their limit to 128,  which causes the headers to be
		fragmented).					G0ORX
	0.5	Reworked hole lists.				G0ORX
	0.6	Changed to Xaw widgets.				G4KLX
		Added automatic file and directory requests.
	0.7	Added cancel fill.				G4KLX
		Added TLM logging.
	0.8	Added directory hole list pruning.		G4KLX
		Added directory entry duplicate checking.
		Added flushing of directory hole file every
		30 seconds.
		Added KISS logging.
*/

#define VERSION_STRING "(version 0.8 by g0orx/n6lyt/g4klx)"

#include <X11/Intrinsic.h>
#include <X11/StringDefs.h>
#include <X11/Shell.h>
#include <X11/Xaw/Cardinals.h>
#include <X11/Xaw/Form.h>
#include <X11/Xaw/Text.h>
#include <X11/Xaw/AsciiText.h>
#include <X11/Xaw/Label.h>
#include <X11/Xaw/Command.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>

#include <netinet/in.h>
#include <linux/ax25.h>

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <ctype.h>
#include <time.h>

#include "xawutils.h"
#include "ftl0.h"
#include "xpb.h"
#include "header.h"
#include "request.h"
#include "crc.h"

Display *dpy;

XtAppContext app_context;

typedef struct
{
	XFontStruct *bold_font, *button_font, *text_font;
}
Resources;

Resources  resources;

Widget toplevel, compwindow, quitbutton, kissbutton, tlmbutton, cancelbutton,
	dirbutton, filebutton, filetext, totallabel, dirlabel, filelabel,
	tlmlabel, crclabel, datawindow;

XtResource resource_list[] =
{
	{"boldFont", XtCFont, XtRFontStruct, sizeof(XFontStruct *),
		XtOffsetOf(Resources, bold_font), XtRString, XtDefaultFont},
	{"buttonFont", XtCFont, XtRFontStruct, sizeof(XFontStruct *),
		XtOffsetOf(Resources, button_font), XtRString, XtDefaultFont},
	{"textFont", XtCFont, XtRFontStruct, sizeof(XFontStruct *),
		XtOffsetOf(Resources, text_font), XtRString, XtDefaultFont}
};

Arg shell_args[] =
{
	{XtNtitle,		(XtArgVal)NULL}
};

Arg form_args[] =
{
	{XtNdefaultDistance,	(XtArgVal)0}
};

Arg button_args[] =
{
	{XtNcallback,		(XtArgVal)NULL},
	{XtNlabel,		(XtArgVal)NULL},
	{XtNfromHoriz,		(XtArgVal)NULL},
	{XtNfont,		(XtArgVal)NULL},
	{XtNresize,		(XtArgVal)False},
	{XtNvertDistance,	(XtArgVal)6},
	{XtNhorizDistance,	(XtArgVal)8},
	{XtNtop,		XtChainTop},
	{XtNbottom,		XtChainTop},
	{XtNleft,		XtChainLeft},
	{XtNright,		XtChainLeft}
};

Arg text_args[] =
{
	{XtNfromHoriz,		(XtArgVal)NULL},
	{XtNfont,		(XtArgVal)NULL},
	{XtNstring,		(XtArgVal)NULL},
	{XtNbackground,		(XtArgVal)NULL},
	{XtNlength,		(XtArgVal)8},
	{XtNwidth,		(XtArgVal)80},
	{XtNvertDistance,	(XtArgVal)6},
	{XtNhorizDistance,	(XtArgVal)8},
	{XtNtop,		XtChainTop},
	{XtNbottom,		XtChainTop},
	{XtNleft,		XtChainLeft},
	{XtNright,		XtChainLeft},
	{XtNeditType,		XawtextEdit},
	{XtNtype,		XawAsciiString},
	{XtNuseStringInPlace,	True}
};

Arg label_args[] =
{
	{XtNfromVert,		(XtArgVal)NULL},
	{XtNfromHoriz,		(XtArgVal)NULL},
	{XtNfont,		(XtArgVal)NULL},
	{XtNwidth,		(XtArgVal)130},
	{XtNlabel,		(XtArgVal)""},
	{XtNheight,		(XtArgVal)25},
	{XtNvertDistance,	(XtArgVal)0},
	{XtNhorizDistance,	(XtArgVal)5},
	{XtNborderWidth,	(XtArgVal)0},
	{XtNjustify,		XtJustifyLeft},
	{XtNresize,		False},
	{XtNtop,		XtChainTop},
	{XtNbottom,		XtChainTop},
	{XtNleft,		XtChainLeft},
	{XtNright,		XtChainLeft}
};

Arg window_args[] =
{
	{XtNfromVert,		(XtArgVal)NULL},
	{XtNbackground,		(XtArgVal)NULL},
	{XtNfont,		(XtArgVal)NULL},
	{XtNcursor,		(XtArgVal)NULL},
	{XtNwidth,		(XtArgVal)600},
	{XtNheight,		(XtArgVal)150},
	{XtNvertDistance,	(XtArgVal)0},
	{XtNhorizDistance,	(XtArgVal)0},
	{XtNtop,		XtChainTop},
	{XtNbottom,		XtChainBottom},
	{XtNleft,		XtChainLeft},
	{XtNright,		XtChainRight},
	{XtNeditType,		XawtextEdit},
	{XtNtype,		XawAsciiString},
	{XtNwrap,		XawtextWrapLine},
	{XtNdisplayNonprinting,	False},
	{XtNdisplayCaret,	False},
	{XtNsensitive,		False}
};

#define	REQUEST_NONE	0
#define	REQUEST_DIR	1
#define	REQUEST_FILE	2

int request_state = REQUEST_NONE;

#define	FILL_NONE	0
#define	FILL_UNIQUE	1
#define	FILL_OVERLAP	2

FILE *fptlm  = NULL;
FILE *fpkiss = NULL;

char writebuf[200];
char id[10];

char satelliteId[16];
char myCall[16];

int s_raw;
int s_file;
int s_directory;
struct sockaddr_ax25 dest;
struct sockaddr_ax25 src;

struct sockaddr_in request_addr;
int s_request;

#define	TIMEOUT		30

unsigned char buf[MAXBUFFER];
int bufSize;


OPENFILES of[MAXFILES + 1];	/* last entry is for the directory file */
int nextof = 0;

unsigned long maxDays = 5;

int bytes     = 0;
int fileBytes = 0;
int dirBytes  = 0;
int tlmBytes  = 0;
int crcErrors = 0;

time_t start_time = 0;
time_t end_time   = 0;

void FillDirectory(void);
void FillFile(void);
void CancelFill(void);

void writetext(char *text)
{
	XawTextPosition pos;
	XawTextBlock tt;

	tt.firstPos = 0;
	tt.ptr      = text;
	tt.length   = strlen(text);
	tt.format   = FMT8BIT;

	pos = XawTextGetInsertionPoint(datawindow);

	XawTextReplace(datawindow, pos, pos, &tt);

	pos += tt.length;
	XawTextSetInsertionPoint(datawindow, pos);
}

/*
 *	Convert a call from the shifted ascii form used in an
 *	AX.25 packet.
 */
int ConvertCall(char *c, char *call)
{
	char *ep = c + 6;
	int ct = 0;

	while (ct < 6)
	{
		if (((*c >> 1) & 127) == ' ') break;

		*call = (*c >> 1) & 127;
		call++;
		ct++;
		c++;
	}
	
	if ((*ep & 0x1E) != 0)
	{	
		*call = '-';
		call++;
		call += sprintf(call, "%d", (int)(((*ep) >> 1) & 0x0F));
	}

	*call = '\0';
	
	if (*ep & 1) return 0;

	return 1;
}

/*
 *	Convert a call to the shifted ascii form used in an
 *	AX.25 packet.
 */
int MakeAddress(char *name, struct sockaddr_ax25 *sax)
{
	int ct = 0;
	int ssid = 0;
	char *p = name;

	while (ct < 6)
	{
		if (*p == '-' || *p == '\0') break;

		if (islower(*p)) *p = toupper(*p);

		if (!isalnum(*p))
		{
			printf("Invalid symbol in callsign %c\n", *p);
			return -1;
		}
		
		sax->sax25_call.ax25_call[ct] = (*p << 1);
		p++;
		ct++;
	}

	while (ct < 6)
	{
		sax->sax25_call.ax25_call[ct] = ' ' << 1;
		ct++;
	}

	if (*p == '-')
	{
		p++;

		if (sscanf(p, "%d", &ssid) != 1 || ssid < 0 || ssid > 15)
		{
			printf("Invalid SSID - %s - %s\n", name, p);
			return -1;
		}
	}

	sax->sax25_call.ax25_call[6]=((ssid + '0') << 1) & 0x1E;
	sax->sax25_family = AF_AX25;

	return 0;
}

void UpdateStatus(void)
{
	static int lastTotal = -1;
	static int lastDir   = -1;
	static int lastFile  = -1;
	static int lastTLM   = -1;
	static int lastCRC   = -1;
	char label[30];
	Arg args[1];

	if (bytes != lastTotal)
	{
		sprintf(label, "Total bytes %8d", bytes);
		
		XtSetArg(args[0], XtNlabel, label);
		XtSetValues(totallabel, args, ONE);

		lastTotal = bytes;
	}

	if (fileBytes != lastFile)
	{
		sprintf(label, "File bytes %7d", fileBytes);
		
		XtSetArg(args[0], XtNlabel, label);
		XtSetValues(filelabel, args, ONE);

		lastFile = fileBytes;
	}

	if (dirBytes != lastDir)
	{
		sprintf(label, "Dir bytes %7d", dirBytes);
		
		XtSetArg(args[0], XtNlabel, label);
		XtSetValues(dirlabel, args, ONE);

		lastDir = dirBytes;
	}

	if (tlmBytes != lastTLM)
	{
		sprintf(label, "TLM bytes %6d", tlmBytes);
		
		XtSetArg(args[0], XtNlabel, label);
		XtSetValues(tlmlabel, args, ONE);

		lastTLM = tlmBytes;
	}

	if (crcErrors != lastCRC)
	{
		sprintf(label, "CRC errors %3d", crcErrors);
		
		XtSetArg(args[0], XtNlabel, label);
		XtSetValues(crclabel, args, ONE);

		lastCRC = crcErrors;
	}
}

/*
 * Hole list maintenance
 */
int LoadHoleList(int f, char *holeName, char *fileName)
{
	FILE *holeFile;
	FILE *fileFile;
	HOLE *hole;
	HOLE *prevHole;
	int i;

	holeFile = fopen(holeName, "r");
	fileFile = fopen(fileName, "r");

	/*
	 * handle the case where a .act file or a .hol file exist without
	 * the other. Usually caused by a crash.
	 */

	if (holeFile != NULL && fileFile != NULL)
	{
		fscanf(holeFile, "%d %*s %*s %*s", &of[f].hdrSeen);
		fscanf(holeFile, "%d %*s %*s %*s", &of[f].fileSize);
		fscanf(holeFile, "%d %*s", &of[f].holes);

		prevHole = NULL;

		for (i = 0; i < of[f].holes; i++)
		{
			hole           = (HOLE *)XtMalloc(sizeof(HOLE));
			hole->nextHole = NULL;
			fscanf(holeFile, "%u, %u", &hole->start, &hole->finish);

			if (prevHole == NULL)
				of[f].firstHole = hole;
			else
				prevHole->nextHole = hole;

			prevHole = hole;
		}
	
		fclose(holeFile);
		fclose(fileFile);

		return(1);
	}
	else
	{
		if (fileFile != NULL) fclose(fileFile);
		if (holeFile != NULL) fclose(holeFile);
	
		/* create an empty hole list */
		of[f].firstHole           = (HOLE *)XtMalloc(sizeof(HOLE));
		of[f].firstHole->start    = 0;
		of[f].firstHole->finish   = 0xFFFFFFFF;
		of[f].firstHole->nextHole = NULL;
		of[f].holes               = 1;

		return(0);
	}
}

void LoadDirectoryHoleList(void)
{
	FILE *holeFile;
	HOLE *hole;
	HOLE *prevHole;
	time_t t;
	int holes;
	unsigned oldest;
	unsigned start, finish;

	/* prune back the list */
	time(&t);
	oldest = (unsigned)t - (60 * 60 * 24 * maxDays);

	if ((holeFile = fopen("pfhdir.hol", "r")) != NULL)
	{
		fscanf(holeFile, "%d %*s %*s %*s", &of[MAXFILES].hdrSeen);
		fscanf(holeFile, "%d %*s %*s %*s", &of[MAXFILES].fileSize);
		fscanf(holeFile, "%d %*s", &holes);

		prevHole = NULL;

		of[MAXFILES].holes = 0;

		while (holes-- > 0)
		{
			fscanf(holeFile, "%u, %u", &start, &finish);

			if (finish > oldest)
			{
				hole           = (HOLE *)XtMalloc(sizeof(HOLE));
				hole->nextHole = NULL;

				hole->start    = (start < oldest) ? oldest : start;
				hole->finish   = finish;

				if (prevHole == NULL)
					of[MAXFILES].firstHole = hole;
				else
					prevHole->nextHole = hole;

				prevHole = hole;

				of[MAXFILES].holes++;
			}
		}
	
		fclose(holeFile);
	}
	else
	{
		/* create an empty hole list */
		of[MAXFILES].firstHole           = (HOLE *)XtMalloc(sizeof(HOLE));
		of[MAXFILES].firstHole->start    = oldest;
		of[MAXFILES].firstHole->finish   = 0xFFFFFFFF;
		of[MAXFILES].firstHole->nextHole = NULL;
		of[MAXFILES].holes               = 1;

		/* use file size as file number - start at 1 */
		of[MAXFILES].fileSize            = 1;
	}
}

void SaveHoleList(int f, char *fileName)
{
	char buffer[40];
	FILE *holeFile;
	HOLE *hole;

	if ((holeFile = fopen(fileName, "w")) == NULL)
	{
		sprintf(buffer, "Cannot open file %s", fileName);
		MessageBox(buffer);
		return;
	}
	
	fprintf(holeFile, "%d pfh header received\n", of[f].hdrSeen);
	fprintf(holeFile, "%d pfh file length\n", of[f].fileSize);
	fprintf(holeFile, "%d holes\n", of[f].holes);

	hole = of[f].firstHole;

	while (hole != NULL)
	{
		fprintf(holeFile, "%u, %u\n", hole->start, hole->finish);
		hole = hole->nextHole;
	}

	fclose(holeFile);
}

int UpdateHoleList(int f, unsigned start, unsigned finish)
{
	HOLE *h, *p, *n;

	for (p = NULL, h = of[f].firstHole; h != NULL; p = h, h = h->nextHole)
	{
		/* see if this is a candidate */
		if (start <= h->finish && finish >= h->start)
		{
			if (start == h->start && finish == h->finish)
			{
				/* exact match - will remove all of it */
				if (p == NULL)
					of[f].firstHole = h->nextHole;
				else
					p->nextHole = h->nextHole;

				of[f].holes--;
				XtFree((char *)h);
				return(FILL_UNIQUE);
			}

			if (start <= h->start && finish >= h->finish)
			{
				/* larger - will remove all of it */
				if (p == NULL)
					of[f].firstHole = h->nextHole;
				else
					p->nextHole = h->nextHole;

				of[f].holes--;
				XtFree((char *)h);
				return(FILL_OVERLAP);
			}

			if (start == h->start && finish < h->finish)
			{
				/* remove the front part */
				h->start = finish + 1;
				return(FILL_UNIQUE);
			}

			if (start < h->start && finish < h->finish)
			{
				/* remove the front part */
				h->start = finish + 1;
				return(FILL_OVERLAP);
			}

			if (start > h->start && finish == h->finish)
			{
				/* will remove the end */
				h->finish = start - 1;
				return(FILL_UNIQUE);
			}

			if (start > h->start && finish > h->finish)
			{
				/* will remove the end */
				h->finish = start - 1;
				return(FILL_OVERLAP);
			}

			if (start > h->start && finish < h->finish)
			{
				/* remove the middle */
				n = (HOLE *)XtMalloc(sizeof(HOLE));
				n->start    = finish + 1;
				n->finish   = h->finish;
				n->nextHole = h->nextHole;

				/* change the current hole for the first part */
				h->finish   = start - 1;
				h->nextHole = n;

				/* one more hole */
				of[f].holes++;
				return(FILL_UNIQUE);
			}
		}
	}

	return(FILL_NONE);
}

/*
 * Message file maintenance
 */
void LoadFile(int fileId, int f)
{
	char buffer[80];
	char holeName[40];
	char fileName[40];

	/* save this file id */
	of[f].fileId = fileId;

	/* load the hole file and open the data file */
	sprintf(fileName, "%x.act", of[f].fileId);
	sprintf(holeName, "%x.hol", of[f].fileId);

	if (LoadHoleList(f, holeName, fileName))
	{
		if ((of[f].file = fopen(fileName, "r+")) == NULL)
		{
			sprintf(buffer, "Cannot open file %s", fileName);
			MessageBox(buffer);
		}
	}
	else
	{
		if ((of[f].file = fopen(fileName, "w+")) == NULL)
		{
			sprintf(buffer, "Cannot open file %s", fileName);
			MessageBox(buffer);
		}
	}
}

void SaveFile(int f)
{
	HOLE *hole;
	HOLE *next;
	char fileName[40];

	if (of[f].file != NULL)
	{
		sprintf(writebuf, "saving file %x\n", of[f].fileId);
		writetext(writebuf);

		/* close the data file */
		fclose(of[f].file);

		/* write out the hole list */
		sprintf(fileName, "%x.hol", of[f].fileId);
		SaveHoleList(f, fileName);

		/* clean up the file table */
		of[f].fileId   = 0;
		of[f].file     = NULL;
		of[f].hdrSeen  = 0;
		of[f].fileSize = 0;
		of[f].holes    = 0;

		/* free the hole list */
		hole = of[f].firstHole;

		while (hole != NULL)
		{
			next = hole->nextHole;
			XtFree((char *)hole);
			hole = next;
		}

		of[f].firstHole = NULL;
	}
}

void CheckDownloaded(int f)
{
	int headerSize;
	HEADER *hdr;
	HOLE *h;
	unsigned char *buf;
	char oldName[80];
	char newName[80];
	char holeName[80];

	/* see if we have the header */
	if (of[f].hdrSeen == 0)
	{
		h = of[f].firstHole;

		if (h->start != 0)
		{
			/* read the header into a buffer */
			if ((buf = XtMalloc(h->start)) != NULL)
			{
				fseek(of[f].file, 0L, SEEK_SET);
				fread(buf, 1, h->start, of[f].file);

				if ((hdr = ExtractHeader(buf, h->start, &headerSize)) != NULL)
				{
					of[f].hdrSeen  = 1;
					of[f].fileSize = hdr->fileSize;
					UpdateHoleList(f, (unsigned)of[f].fileSize, (unsigned)0xFFFFFFFF);
					XtFree((char *)hdr);
				}

				XtFree(buf);
			}
		}
	}

	/* see if we now have the complete file */
	if (of[f].holes == 0 && of[f].hdrSeen == 1)
	{
		unsigned long fileId;

		/* close the current file */
		fileId = of[f].fileId;
		SaveFile(f);

		/* rename the file */
		sprintf(oldName, "%lx.act", fileId);
		sprintf(newName, "%lx.dl", fileId);
		rename(oldName, newName);

		/* remove the hole file */
		sprintf(holeName, "%lx.hol", fileId);
		unlink(holeName);

		/* let the user know */
		sprintf(writebuf, "%lx downloaded\n", fileId);
		writetext(writebuf);
	}
}

void BroadcastFile(unsigned char *buffer, int length)
{
	static unsigned HeardId = 0;
	static unsigned DupId   = 0;
	FILEHEADER *fh;
	unsigned char *data;
	unsigned dataOffset;
	unsigned dataLength;
	char fileName[80];
	FILE *fp;
	int i;

	/* crc validation */
	if (!CheckCRC(buffer, length))
	{
		crcErrors++;
		UpdateStatus();
		return;
	}

	/* point to the header */
	fh = (FILEHEADER *)buffer;

	/* point to the data */
	data       = buffer + sizeof(FILEHEADER);
	dataOffset = fh->wOffset + (fh->nOffsetHigh << 16);
	dataLength = length - sizeof(FILEHEADER) - CRCLENGTH;
	
	/* inform the user */
	if (fh->fileId != HeardId)
	{
		sprintf(writebuf, "heard message %x\n", fh->fileId);
		writetext(writebuf);
		HeardId = fh->fileId;
		DupId   = -1;
	}

	/* see if the file already downloaded */
	sprintf(fileName, "%x.dl", fh->fileId);

	if ((fp = fopen(fileName, "r")) != NULL)
	{
		fclose(fp);

		if (fh->fileId != DupId)
		{
			sprintf(writebuf, "%x is already downloaded\n", fh->fileId);
			writetext(writebuf);
			DupId = fh->fileId;
		}

		return;
	}

	/* see if the file is in the current list of files */
	for (i = 0; i < MAXFILES; i++)
		if (of[i].fileId == fh->fileId)
			break;

	/* if it is not there then we must load it */
	if (i == MAXFILES)
	{
		/* save current file if used */
		SaveFile(nextof);

		/* now use this slot */
		LoadFile(fh->fileId, nextof);
		
		/* setup i for this entry */
		i = nextof;

		/* make sure we round robin the open files */
		nextof = (nextof + 1) % MAXFILES;
	}

	if (UpdateHoleList(i, (unsigned)dataOffset, (unsigned)(dataOffset + dataLength - 1)) != FILL_NONE)
	{
		/* write the data */
		fseek(of[i].file, dataOffset, SEEK_SET);
		fwrite(data, 1, dataLength, of[i].file);

		/* see if it has all been downloaded */
		CheckDownloaded(i);
	}
}

/*
 *	Directory Maintenance
 */
void WriteDirectory(unsigned char *data, int dataLength)
{
	char buffer[80];
	char fileName[40];
	struct stat statbuf;
	FILE *fp;

	sprintf(fileName, "pb__%04d.pfh", of[MAXFILES].fileSize);

	/* write the data to the end of the current file */
	if ((fp = fopen(fileName, "a")) == NULL)
	{
		sprintf(buffer, "Cannot open file %s", fileName);
		MessageBox(buffer);
		return;
	}

	fwrite(data, 1, dataLength, fp);
	fclose(fp);

	stat(fileName, &statbuf);

	/* limit file size to 20000 bytes */
	if (statbuf.st_size > 20000) of[MAXFILES].fileSize++;
}

void BroadcastDirectory(unsigned char *buffer, int length)
{
	static unsigned int  fragmentId     = 0;
	static unsigned int  fragmentOffset = 0;
	static unsigned char fragmentBuffer[1024];
	DIRHEADER *dh;
	int headerSize;
	HEADER *hdr;
	unsigned char *data;
	int dataLength;
	int i;

	/* crc validation */
	if (!CheckCRC(buffer, length))
	{
		crcErrors++;
		UpdateStatus();
		return;
	}

	/* point to the header */
	dh = (DIRHEADER *)buffer;

	/* check for fragmented directory header */
	if (dh->offset != 0)
	{
		/* see if this is the next fragment */
		if (fragmentId == dh->fileId && dh->offset == fragmentOffset)
		{
			/* append this fragment */
			data       = buffer + sizeof(DIRHEADER);
			dataLength = length - sizeof(DIRHEADER) - CRCLENGTH;

			for (i = 0; i < dataLength; i++)
				fragmentBuffer[fragmentOffset + i] = data[i];

			fragmentOffset += dataLength;

			if ((dh->flags & LASTBYTEFLAG) != LASTBYTEFLAG)
			{
				data       = fragmentBuffer;
				dataLength = fragmentOffset;
			}
			else
			{
				return;
			}
		}
		else
		{
			fragmentId     = 0;
			fragmentOffset = 0;
			return;
		}
	}
	else if ((dh->flags & LASTBYTEFLAG) != LASTBYTEFLAG)
	{
		/* copy the fragment */
		data       = buffer + sizeof(DIRHEADER);
		dataLength = length - sizeof(DIRHEADER) - CRCLENGTH;

		for (i = 0; i < dataLength; i++)
			fragmentBuffer[i] = data[i];

		/* start a new fragment */
		fragmentId     = dh->fileId;
		fragmentOffset = dataLength;

		return;
	}
	else
	{
		/* its all there -  point to the data */
		data       = buffer + sizeof(DIRHEADER);
		dataLength = length - sizeof(DIRHEADER) - CRCLENGTH;
	}

	if ((dh->flags & LASTBYTEFLAG) == LASTBYTEFLAG)
	{
		/* reset fragmentation */
		fragmentId     = 0;
		fragmentOffset = 0;

		/* try to extract the header */
		if ((hdr = ExtractHeader(data, dataLength, &headerSize)) != NULL)
		{
			/* inform the user */
			if (strlen(hdr->title) == 0)
				sprintf(writebuf, "dir: %lx: from:%s to:%s title:%s\n",
					hdr->fileId,
					hdr->source,
					hdr->destination,
					hdr->fileName);
			else
				sprintf(writebuf, "dir: %lx: from:%s to:%s title:%s\n",
					hdr->fileId,
					hdr->source,
					hdr->destination,
					hdr->title);

			writetext(writebuf);

			/* see if the hole list is loaded */
			if (of[MAXFILES].firstHole == NULL)
				LoadDirectoryHoleList();

			/* update the directory  if needed */
			if (UpdateHoleList(MAXFILES, dh->tOld, dh->tNew) == FILL_UNIQUE)
				WriteDirectory(data, dataLength);

			XtFree((char *)hdr);
		}
		else
		{
			sprintf(writebuf, "** Bad directory header for %x\n", dh->fileId);
			writetext(writebuf);
		}
	}
}

/*
 *	decode a received frame.
 */
void ProcessFrame(void)
{
	int n, i;
	int via;
	unsigned char protocol;
	char toCall[10];
	char fromCall[10];
	char viaCall[10];
	char *s;

	/* check that frame is a kiss data frame */
	/* ignore control frames - should not happen */
	n = 0;

	if ((buf[n] & 0x0F) == 0)
	{
		n++;

		bytes += bufSize - 1;

		if (fpkiss != NULL)
		{
			fputc(0300, fpkiss);		/* FEND */
			fputc(0x00, fpkiss);		/* KISS Data */

			for (i = n; i < bufSize; i++)
			{
				switch (buf[i])
				{
					case 0300:	/* FEND */
						fputc(0333, fpkiss);	/* FESC */
						fputc(0334, fpkiss);	/* FESC_END */
						break;
					case 0333:	/* FESC */
						fputc(0333, fpkiss);	/* FESC */
						fputc(0335, fpkiss);	/* FESC_ESC */
						break;
					default:
						fputc(buf[i], fpkiss);
						break;
				}
			}

			fputc(0300, fpkiss);		/* FEND */
		}

		/* decode the to/from address */
		/* dont expect via address, but saves last if any */
		via = ConvertCall(buf + n, toCall);
		n += 7;

		via = ConvertCall(buf + n, fromCall);
		n += 7;

		while (via)
		{
			via = ConvertCall(buf + n, viaCall);
			n += 7;
		}

		/* check for a UI frame */
		if ((buf[n] & 0xEF) == 0003)
		{
			n++;
			protocol = buf[n++];

			/* see if the frame is a broadcast frame */	
			if (strcmp(toCall, "QST-1") == 0)
			{
				switch (protocol)
				{
				case PID_FILE:
					fileBytes += bufSize - n;
					BroadcastFile(buf + n,  bufSize - n);
					break;
				case PID_DIRECTORY:
					dirBytes += bufSize - n;
					BroadcastDirectory(buf + n, bufSize - n);
					break;
				default:
					break;
				}
			}
			else if (strcmp(toCall, "PBLIST") == 0)
			{
				buf[bufSize] = '\0';
				sprintf(writebuf, "%s\n", buf + n);
				writetext(writebuf);

				if (strstr(buf + n, myCall) == NULL)
				{
					switch (request_state)
					{
						case REQUEST_DIR:
							FillDirectory();
							break;
						case REQUEST_FILE:
							FillFile();
							break;
					}
				}
				else
				{
					request_state = REQUEST_NONE;
				}
			}
			else if (strcmp(toCall, "TLM") == 0)
			{
				tlmBytes += bufSize - n;

				if (!CheckCRC(buf + n, bufSize - n))
				{
					crcErrors++;
					UpdateStatus();
					return;
				}

				if (fptlm != NULL)
				{
					fputc((bufSize - n) % 256, fptlm);
					fputc((bufSize - n) / 256, fptlm);
					fwrite(buf + n, 1, bufSize - n, fptlm);
				}
			}
			else
			{
				buf[bufSize] = '\0';
				sprintf(writebuf, "%s\n", buf + n);
				writetext(writebuf);

				if (request_state == REQUEST_FILE &&
				    protocol      == PID_FILE     &&
				    strcmp(toCall, myCall) == 0   &&
				    strncmp(buf + n, "NO -2 ", 6) == 0)
				{
					request_state = REQUEST_NONE;
					sprintf(writebuf, "** File %s not present, request cancelled\n", id);
					writetext(writebuf);
				}
			}
		}
	}

	UpdateStatus();
}

/*
 *	callback function when a frame is received.
 */
void GetFrame(XtPointer closure, int *s, XtInputId *Id)
{
	if ((bufSize = recv(s_raw, buf, MAXBUFFER, 0)) == -1)
	{
		perror("recv");
		return;
	}

	time(&end_time);
	
	if (start_time == 0) start_time = end_time;

	ProcessFrame();
}

/*
 * Timeout has occured, flush the directory hole list.
 */
void TimeoutCb(XtPointer closure, XtIntervalId *id)
{
#ifdef FLUSH_FILES
	char fileName[40];
	int i;

	for (i = 0; i < MAXFILES; i++)
	{
		if (of[i].file != NULL)
		{
			/* write out the hole list */
			sprintf(fileName, "%x.hol", of[i].fileId);
			SaveHoleList(i, fileName);
		}
	}
#endif

	/* save the directory hole list */
	if (of[MAXFILES].firstHole != NULL)
		SaveHoleList(MAXFILES, "pfhdir.hol");

	XtAppAddTimeOut(app_context, TIMEOUT * 1000, TimeoutCb, NULL);
}

/*
 *	callback function when a another app calls us
 */
void FillRequest(XtPointer closure, int *s, XtInputId *Id)
{
	struct request request;

	if (recv(s_request, (char *)&request, sizeof(struct request), 0) == sizeof(struct request))
	{
		switch (request.type)
		{
			case REQ_TYPE_CANCEL:
				CancelFill();
				break;
			case REQ_TYPE_DIR:
				FillDirectory();
				break;
			case REQ_TYPE_FILE:
				sprintf(id, "%lx", request.fileId);
				XtVaSetValues(filetext, XtNstring, id, NULL);
				XawTextSetInsertionPoint(filetext, strlen(id));
				FillFile();
				break;
		}
	}
	else
	{
		perror("recv - FillRequest");
	}
}

/*
 *	the user wants to exit this program
 */
void QuitCb(Widget w, XtPointer client_data, XtPointer call_data)
{
	FILE *fp;
	struct tm *tm;
	int ave_rate = 0, i;

	if (fptlm != NULL)  fclose(fptlm);
	if (fpkiss != NULL) fclose(fpkiss);

	/* save any open files */
	for (i = 0; i < MAXFILES; i++)
		SaveFile(i);

	/* save the directory hole list */
	if (of[MAXFILES].firstHole != NULL)
		SaveHoleList(MAXFILES, "pfhdir.hol");

	/* close the sockets */
	close(s_raw);
	close(s_file);
	close(s_directory);

	/* write the statistics out */
	if (start_time != 0)
	{
		if ((fp = fopen("pb.log", "a")) != NULL)
		{
			if (end_time - start_time > 0)
				ave_rate = bytes / (end_time - start_time);

			tm = gmtime(&start_time);
			fprintf(fp, "%02d/%02d/%02d %02d:%02d:%02d - ",
				tm->tm_mday, tm->tm_mon + 1, tm->tm_year,
				tm->tm_hour, tm->tm_min, tm->tm_sec);

			tm = gmtime(&end_time);
			fprintf(fp, "%02d:%02d:%02d - total %d files %d dir %d tlm %d ave %d crc %d\n",
				tm->tm_hour, tm->tm_min, tm->tm_sec,
				bytes, fileBytes, dirBytes, tlmBytes, ave_rate, crcErrors);
	
			fclose(fp);
		}
	}

	XtDestroyApplicationContext(app_context);

	exit(0);
}

void KissCb(Widget w, XtPointer client_data, XtPointer call_data)
{
	char fileName[20];
	Arg args[1];
	time_t t;

	if (fpkiss == NULL)
	{
		time(&t);
		
		sprintf(fileName, "%x.kss", (int)t);
		
		if ((fpkiss = fopen(fileName, "w")) == NULL)
			return;
			
		XtSetArg(args[0], XtNlabel, "KISS Log On");
	}
	else
	{
		fclose(fpkiss);
		fpkiss = NULL;

		XtSetArg(args[0], XtNlabel, "KISS Log Off");
	}

	XtSetValues(kissbutton, args, ONE);
}

void TLMCb(Widget w, XtPointer client_data, XtPointer call_data)
{
	char fileName[20];
	Arg args[1];
	time_t t;

	if (fptlm == NULL)
	{
		time(&t);

		sprintf(fileName, "%x.tlm", (int)t);

		if ((fptlm = fopen(fileName, "w")) == NULL)
			return;
			
		XtSetArg(args[0], XtNlabel, "TLM Log On");
	}
	else
	{
		fclose(fptlm);
		fptlm = NULL;

		XtSetArg(args[0], XtNlabel, "TLM Log Off");
	}

	XtSetValues(tlmbutton, args, ONE);
}

/*
 *	Construct a fill request for the directory.
 */
void FillDirectory(void)
{
	DIRREQUESTHEADER *request;
	DIRHOLEPAIR *hole;
	HOLE *h;
	unsigned char *buffer;
	int i;

	/* see if the hole list is loaded */
	if (of[MAXFILES].firstHole == NULL)
		LoadDirectoryHoleList();

	/* construct a hole list request */
	buffer = (unsigned char *)XtMalloc(256);
	request = (DIRREQUESTHEADER *)buffer;

	request->flags     = 0x00 | VERSION | 0x10;
	request->blockSize = 244;

	hole = (DIRHOLEPAIR *)&buffer[sizeof(DIRREQUESTHEADER)];
	h = of[MAXFILES].firstHole;
	for (i = 0; i < 10 && i < of[MAXFILES].holes; i++)
	{
		hole->startTime = h->start;
		hole->endTime   = h->finish;
		h = h->nextHole;
		hole++;
	}

	if (sendto(s_directory, buffer,
		   sizeof(DIRREQUESTHEADER) + (i * sizeof(DIRHOLEPAIR)),
		   0, (struct sockaddr *)&dest, sizeof(dest)) == -1)
		perror("send");

	sprintf(writebuf, "Fill Directory: fill %d holes.\n", i);
	writetext(writebuf);

	XtFree(buffer);

	request_state = REQUEST_DIR;
}

void FillDirectoryCb(Widget w, XtPointer client_data, XtPointer call_data)
{
	FillDirectory();
}

/*
 *	construct a fill request for a file
 */
void FillFile(void)
{
	unsigned int fileId;
	char fileName[80];
	unsigned char *buffer;
	REQUESTHEADER *request;
	HOLEPAIR *hole;
	HOLE *h;
	FILE *fp;
	int f, i;

	if (strlen(id) == 0) return;
	sscanf(id, "%x", &fileId);
	if (fileId == 0) return;

	/* see if the file already downloaded */
	sprintf(fileName, "%x.dl", fileId);
	if ((fp = fopen(fileName, "r")) != NULL)
	{
		fclose(fp);
		sprintf(writebuf, "%x is already downloaded\n", fileId);
		writetext(writebuf);
		return;
	}

	/* see if the file is in the current list of files */
	for (f = 0; f < MAXFILES; f++)
		if (of[f].fileId == fileId)
			break;

	if (f == MAXFILES)
	{
		/* not there - must save if this is in use */
		SaveFile(nextof);

		/* now use this slot */
		LoadFile(fileId, nextof);

		/* make sure we round robin the files */
		f = nextof;
		nextof = (nextof + 1) % MAXFILES;
	}

	buffer  = (unsigned char *)XtMalloc(256);
	request = (REQUESTHEADER *)buffer;

	if (of[f].holes == 1 && of[f].firstHole->start  == 0 &&
				of[f].firstHole->finish == 0xFFFFFFFF)
	{
		request->flags     = 0x00 | VERSION | 0x10;
		request->fileId    = fileId;
		request->blockSize = 244;

		if (sendto(s_file, buffer, sizeof(REQUESTHEADER), 0,
		    (struct sockaddr *)&dest, sizeof(dest)) == -1)
			perror("send");

		sprintf(writebuf, "Fill File: %x send file.\n", fileId);
		writetext(writebuf);
	}
	else
	{
		request->flags     = 0x02 | VERSION | 0x10;
		request->fileId    = fileId;
		request->blockSize = 244;

		hole = (HOLEPAIR *)&buffer[sizeof(REQUESTHEADER)];
		h = of[f].firstHole;

		for (i = 0; i < 10 && i < of[f].holes; i++)
		{
			hole->offset     = h->start;
			hole->offset_msb = h->start >> 16;
			hole->length     = 1 + (h->finish - h->start);
			h = h->nextHole;
			hole++;
		}

		if (sendto(s_file, buffer,
			    sizeof(REQUESTHEADER) + (i * sizeof(HOLEPAIR)), 0,
			    (struct sockaddr *)&dest, sizeof(dest)) == -1)
			perror("send");

		sprintf(writebuf, "Fill File: %x fill %d holes.\n", fileId, i);
		writetext(writebuf);
	}

	XtFree(buffer);

	request_state = REQUEST_FILE;
}

void FillFileCb(Widget w, XtPointer client_data, XtPointer call_data)
{
	FillFile();
}

void CancelFill(void)
{
	switch (request_state)
	{
		case REQUEST_DIR:
			writetext("Cancel Fill: directory\n");
			break;
		case REQUEST_FILE:
			sprintf(writebuf, "Cancel Fill: file %s\n", id);
			writetext(writebuf);
			break;
		default:
			break;
	}

	request_state = REQUEST_NONE;
}

void CancelFillCb(Widget w, XtPointer client_data, XtPointer call_data)
{
	CancelFill();
}

int main(int argc, char **argv)
{
	static XtCallbackRec callback[2];
	int n;
	char *s, title[80];

	if ((s = getenv("SATELLITE")) == NULL)
	{
		printf("SATELLITE environment variable not set.\n");
		return(1);
	}

	strcpy(satelliteId, s);

	if ((s = getenv("MYCALL")) == NULL)
	{
		printf("MYCALL environment variable not set.\n");
		return(1);
	}

	strcpy(myCall, s);

	if ((s = getenv("MAXDAYS")) != NULL)
	{
		maxDays = atoi(s);
		if (maxDays <= 0) maxDays = 5;
	}

	sprintf(title, "xpb:%s %s", satelliteId, VERSION_STRING);

	strcat(satelliteId, "-11");
	MakeAddress(myCall, &src);
	MakeAddress(satelliteId, &dest);

	toplevel = XtAppInitialize(&app_context, "Xpb", NULL, 0, &argc, argv,
				NULL, shell_args, XtNumber(shell_args));
	XtVaSetValues(toplevel, XtNtitle, title, NULL);

	dpy  = XtDisplay(toplevel);

	XtGetApplicationResources(toplevel, &resources,
				resource_list, XtNumber(resource_list),
				NULL, ZERO);

	compwindow = XtCreateManagedWidget("appForm", formWidgetClass,
				toplevel, form_args, XtNumber(form_args));

	callback[0].callback = QuitCb;
	callback[0].closure  = toplevel;
	button_args[0].value = (XtArgVal)callback;
	button_args[1].value = (XtArgVal)"Quit";
	button_args[3].value = (XtArgVal)resources.button_font;
	quitbutton = XtCreateManagedWidget("quitButton", commandWidgetClass,
				compwindow, button_args, XtNumber(button_args));

	callback[0].callback = KissCb;
	callback[0].closure  = toplevel;
	button_args[0].value = (XtArgVal)callback;
	button_args[1].value = (XtArgVal)"KISS Log Off";
	button_args[2].value = (XtArgVal)quitbutton;
	kissbutton = XtCreateManagedWidget("kissButton", commandWidgetClass,
				compwindow, button_args, XtNumber(button_args));

	callback[0].callback = TLMCb;
	callback[0].closure  = toplevel;
	button_args[0].value = (XtArgVal)callback;
	button_args[1].value = (XtArgVal)"TLM Log Off";
	button_args[2].value = (XtArgVal)kissbutton;
	tlmbutton = XtCreateManagedWidget("tlmButton", commandWidgetClass,
				compwindow, button_args, XtNumber(button_args));

	callback[0].callback = CancelFillCb;
	callback[0].closure  = toplevel;
	button_args[0].value = (XtArgVal)callback;
	button_args[1].value = (XtArgVal)"Cancel Fill";
	button_args[2].value = (XtArgVal)tlmbutton;
	cancelbutton = XtCreateManagedWidget("cancelButton", commandWidgetClass,
				compwindow, button_args, XtNumber(button_args));

	callback[0].callback = FillDirectoryCb;
	callback[0].closure  = toplevel;
	button_args[0].value = (XtArgVal)callback;
	button_args[1].value = (XtArgVal)"Fill Directory";
	button_args[2].value = (XtArgVal)cancelbutton;
	dirbutton = XtCreateManagedWidget("dirButton", commandWidgetClass,
				compwindow, button_args, XtNumber(button_args));

	callback[0].callback = FillFileCb;
	callback[0].closure  = toplevel;
	button_args[0].value = (XtArgVal)callback;
	button_args[1].value = (XtArgVal)"Fill File";
	button_args[2].value = (XtArgVal)dirbutton;
	filebutton = XtCreateManagedWidget("fileButton", commandWidgetClass,
				compwindow, button_args, XtNumber(button_args));

	id[0] = '\0';
	text_args[0].value = (XtArgVal)filebutton;
	text_args[1].value = (XtArgVal)resources.text_font;
	text_args[2].value = (XtArgVal)id;
	text_args[3].value = (XtArgVal)WhitePixel(dpy, DefaultScreen(dpy));
	filetext = XtCreateManagedWidget("fileidText", asciiTextWidgetClass,
				compwindow, text_args, XtNumber(text_args));

	label_args[0].value = (XtArgVal)quitbutton;
	label_args[2].value = (XtArgVal)resources.button_font;
	totallabel = XtCreateManagedWidget("totalLabel", labelWidgetClass,
				compwindow, label_args, XtNumber(label_args));

	label_args[1].value = (XtArgVal)totallabel;
	label_args[3].value = (XtArgVal)115;
	filelabel = XtCreateManagedWidget("fileLabel", labelWidgetClass,
				compwindow, label_args, XtNumber(label_args));

	label_args[1].value = (XtArgVal)filelabel;
	label_args[3].value = (XtArgVal)110;
	dirlabel = XtCreateManagedWidget("dirLabel", labelWidgetClass,
				compwindow, label_args, XtNumber(label_args));

	label_args[1].value = (XtArgVal)dirlabel;
	tlmlabel = XtCreateManagedWidget("tlmLabel", labelWidgetClass,
				compwindow, label_args, XtNumber(label_args));

	label_args[1].value = (XtArgVal)tlmlabel;
	crclabel = XtCreateManagedWidget("crcLabel", labelWidgetClass,
				compwindow, label_args, XtNumber(label_args));

	window_args[0].value = (XtArgVal)totallabel;
	window_args[1].value = (XtArgVal)WhitePixel(dpy, DefaultScreen(dpy));
	window_args[2].value = (XtArgVal)resources.text_font;
	datawindow = XtCreateManagedWidget("monitorText", asciiTextWidgetClass,
				compwindow, window_args, XtNumber(window_args));

	createMessagePopup(resources.bold_font, resources.button_font);

	/* initialize the open files structure */
	for (n = 0; n < MAXFILES; n++)
	{
		of[n].fileId    = 0;
		of[n].file      = NULL;
		of[n].hdrSeen   = 0;
		of[n].fileSize  = 0;
		of[n].holes     = 0;
		of[n].firstHole = NULL;
	}

	/* open up the raw socket to receive all packets */
	if ((s_raw = socket(AF_INET, SOCK_PACKET, htons(2))) == -1)
	{
		perror("socket");
		return(1);
	}

	/* open up the AX25 datagram socket to send file requests */
	if ((s_file = socket(AF_AX25, SOCK_RAW, PID_FILE)) == -1)
	{
		perror("socket");
		return(1);
	}

	if (bind(s_file, (struct sockaddr *)&src, sizeof(src)) == -1)
	{
		perror("bind");
		return(1);
	}

	/* open up the AX25 datagram socket to send directory requests */
	if ((s_directory = socket(AF_AX25, SOCK_RAW, PID_DIRECTORY)) == -1)
	{
		perror("socket");
		return(1);
	}

	if (bind(s_directory, (struct sockaddr *)&src, sizeof(src)) == -1)
	{
		perror("bind");
		return(1);
	}

	/* we want to be notified whenever a frame is received on the raw socket */
	XtAppAddInput(app_context, s_raw, (XtPointer)XtInputReadMask, GetFrame, NULL);

	/* open up a udp socket to receive file fill requests from other apps */
	memset((char *)&request_addr, 0, sizeof(request_addr));
	request_addr.sin_family      = AF_INET;
	request_addr.sin_addr.s_addr = INADDR_ANY;
	request_addr.sin_port        = htons(5100);

	if ((s_request = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
	{
		perror("socket - s_request");
	}
	else
	{
		XtAppAddInput(app_context, s_request, (XtPointer)XtInputReadMask, FillRequest, NULL);
	}
		
	if (bind(s_request, (struct sockaddr *)&request_addr, sizeof(request_addr)) == -1)
	{
		perror("bind - s_request");
	}

	UpdateStatus();

	XtAppAddTimeOut(app_context, TIMEOUT * 1000, TimeoutCb, NULL);

	XtRealizeWidget(toplevel);

	XtAppMainLoop(app_context);
	
	return(0);
}

