/* QAmiTrack.c -- a program to keep a running list of available Amigas. */

#include "QAmiTrackShared.h"
#include "TrackRexx.h"
#include "QAmitrack.h"

#include "include:signal.h"

/* our GUI gadget IDs */
#define GAD_CLIENTLIST    10
#define GAD_VARLIST       11
#define GAD_COMMENTSTRING 12
#define GAD_SERVERSTRING  13
#define GAD_PORTSTRING    14
#define GAD_ACTIONCYCLE   15
#define GAD_LOGLIST       16

/* Menu item IDs */
#define P_ABOUT         100
#define P_HIDE          101
#define P_QUIT          102
#define O_ENABLED       200
#define O_CONFIRM       201
#define O_RAGGED        202
#define O_BEEPONLOG     203
#define O_LOG           204
#define O_LOGNONE       205
#define O_LOGTERSE      206
#define O_LOGMODERATE   207
#define O_LOGVERBOSE    208
#define O_VIEWHOSTNAME  209
#define O_VIEWUSERNAME  210
#define O_VIEWREALNAME  211
#define O_SORTBYAGE     212
#define O_SORTBYNAME    213
#define O_SORTBYCOMMENT 214
#define O_SORTBYNONE    215
#define O_LOGVIEWOFF    216
#define O_LOGVIEW25     217
#define O_LOGVIEW50     218
#define O_LOGVIEW75     219
#define O_LOGVIEWFULL   220
#define O_REVERTCOMMENT 221
#define O_CLEARLOGVIEW  222

/* Not in the menus, but... */
#define O_COMMENT       999

#define Strncat(a,b,c) {strncat(a,b,c); a[c-1] = '\0';}
#define Strncpy(a,b,c) {strncpy(a,b,c); a[c-1] = '\0';}

/* script callback keywords for rexx script tooltypes */
const char * rexxScriptNames[] = {
   "LOGONSCRIPT",
   "LOGOFFSCRIPT",
   "UPDATESCRIPT",
   "CONNECTSCRIPT",
   "DISCONNECTSCRIPT",
   "STARTSCRIPT",
   "ENDSCRIPT",
   "SYSMESSAGESCRIPT",
   "DEFAULTSCRIPT"
};
  
/* Logging levels */
#define OUTPUT_NONE     0
#define OUTPUT_TERSE    1
#define OUTPUT_MODERATE 2
#define OUTPUT_VERBOSE  3

/* chords of what to transmit in SendState() */
#define STATE_REALNAME 0x01
#define STATE_USERNAME 0x02
#define STATE_COMMENT  0x04
#define STATE_WINOPEN  0x08
#define STATE_ALL      0xFF

/* overridable, compile-time defaults */
#define DEFAULT_AMITRACK_SERVER        "qamitrack.tibb.at"
#define DEFAULT_AMITRACK_PORT          2957
#define DEFAULT_AMITRACK_COMMENT       ""
#define DEFAULT_AMITRACK_CXPOPUP       "YES"
#define DEFAULT_AMITRACK_CXPRI         0
#define DEFAULT_AMITRACK_POPKEY        "lcommand shift a"
#define DEFAULT_AMITRACK_ENABLED       "YES"
#define DEFAULT_AMITRACK_ALLOWMULTIPLE "NO"
#define DEFAULT_AMITRACK_CONFIRMAPP    "YES"
#define DEFAULT_AMITRACK_RAGGEDTEXT    "NO"
#define DEFAULT_AMITRACK_BEEPONLOG     "NO"
#define DEFAULT_AMITRACK_RECONNECTDELAY 2
#define DEFAULT_AMITRACK_LOGFILE       ""
#define DEFAULT_AMITRACK_LOGLEVEL      "moderate"
#define DEFAULT_AMITRACK_USERNAME      "(anonymous)"
#define DEFAULT_AMITRACK_VIEWBY        "hostname"
#define DEFAULT_AMITRACK_SORTBY        "age"
#define DEFAULT_AMITRACK_ACCESSTO      "#?"
#define DEFAULT_AMITRACK_LOGVIEWLEVEL  "25%"
#define DEFAULT_AMITRACK_LOGVIEWLENGTH 50

UBYTE ** ttypes = NULL;
char * rexxScripts[NUM_LOG_ENUMS];

int currentAction = -1;
BOOL BConfirmAppLaunch, BConnected = FALSE, BBeepOnLog = FALSE;
BOOL BStartedFromWB = FALSE, BRaggedText;
int clientListLength = 0, logListLen = 0, nLogLevel;
int connectDelayLeft = 0, nReconnectDelay;  /* nReconnectDelay is in minutes */
int nViewBy, nSortBy;

int nPort, nNumActions=0, nLastEntryClicked=-1, currentNameLen = -1;
int timeUnit=60, nUpdateDelay=20;  /* Show in minutes, update display three times per minute */
int nLogViewLevel, maxLogListLen;
struct CxStuff * CX = NULL;
struct Client * myInfo = NULL;
char * szServerName = NULL, * szLogFileName = NULL, * szAccessTo, * origComment = NULL;
struct QSession * session = NULL;
char szVersionString[] = "$VER: QAmiTrack 1.92 (Compiled " __DATE__ ")", * pcDisplayVersionString;
struct TimerStuff * TS = NULL;
ULONG lastUpdateTime = 0L;
struct Client * cLastEntryClicked = NULL;
struct Task * mainTask = NULL;

void Cleanup(void);
void UpdateWindowTimes(void);
void InitActions(void);  /* NULLs all string pointers */
void FreeActions(void);  /* Frees all string pointers */

int main(int argc, char **argv);

/* Data structures for Gadtools GUI */
struct Library * WorkbenchBase = NULL;
struct Library * IconBase      = NULL;
struct Library * GraphicsBase  = NULL;
struct Library * IntuitionBase = NULL;
struct Library * GadToolsBase  = NULL;
struct Library * DiskFontBase  = NULL;
struct Library * CxBase        = NULL;
struct Library * AMarqueeBase  = NULL;

struct RexxHost    * rexxHost    = NULL;
struct WindowStuff * TrackWindow = NULL;
struct Menu        * Menu        = NULL;
struct MenuItem    * MenuItem    = NULL;

struct List clientList, logList;

char * ActionLabels[20], * Actions[20];

/* default GUI settings */
int nWinLeft		= 50;
int nWinTop		    = 100;
int nWinWidth		= 520;
int nWinHeight		= 200;
int nMinWinWidth	= 300;
int nMinWinHeight	= 120;

long lLastChangeAt = 0L;
BOOL BEnabled = TRUE, BAllowMultiple = FALSE;

/* menus */
struct NewMenu nmMenus[] = {
    NM_TITLE, "Project", 	 NULL, 	0L, 	   	 NULL, NULL,
    NM_ITEM,  "About",	 	 "?",	0L,		     NULL, (void *) P_ABOUT,
    NM_ITEM,  "Hide", 	     "H",	0L,		     NULL, (void *) P_HIDE,
    NM_ITEM,  NM_BARLABEL, 	 NULL, 	0L, 	   	 NULL, NULL,
    NM_ITEM,  "Quit",  	 	 "Q",  	0L, 	   	 NULL, (void *) P_QUIT,

    NM_TITLE, "Options",     NULL,  0L,          NULL, NULL,
    NM_ITEM,  "Enabled",     "E",   CHECKIT,     NULL, (void *) O_ENABLED,
    NM_ITEM,  "Confirm",     "C",   CHECKIT,     NULL, (void *) O_CONFIRM,
    NM_ITEM,  "Ragged Text", "T",   CHECKIT,     NULL, (void *) O_RAGGED,
    NM_ITEM,  "Beep On Log", "B",   CHECKIT,     NULL, (void *) O_BEEPONLOG,

    NM_ITEM,  "Log Level",   NULL,  NULL,        NULL, NULL,
    NM_SUB,   "None",         "!",  CHECKIT,     NULL, (void *) O_LOGNONE,
    NM_SUB,   "Terse",        "@",  CHECKIT,     NULL, (void *) O_LOGTERSE,
    NM_SUB,   "Moderate",     "#",  CHECKIT,     NULL, (void *) O_LOGMODERATE,
    NM_SUB,   "Verbose",      "$",  CHECKIT,     NULL, (void *) O_LOGVERBOSE,        

    NM_ITEM,  "View By",     NULL,  0L,          NULL, NULL,
    NM_SUB,   "Hostname",     "N",  CHECKIT,     NULL, (void *) O_VIEWHOSTNAME,
    NM_SUB,   "User Name",    "U",  CHECKIT,     NULL, (void *) O_VIEWUSERNAME,
    NM_SUB,   "Real Name",    "R",  CHECKIT,     NULL, (void *) O_VIEWREALNAME,
    
    NM_ITEM,  "Sort By",     NULL,  0L,          NULL, NULL,
    NM_SUB,   "Age",          "A",  CHECKIT,     NULL, (void *) O_SORTBYAGE,
    NM_SUB,   "Name",         "M",  CHECKIT,     NULL, (void *) O_SORTBYNAME,
    NM_SUB,   "Comment",      "L",  CHECKIT,     NULL, (void *) O_SORTBYCOMMENT,
    NM_SUB,   "None",         "G",  CHECKIT,     NULL, (void *) O_SORTBYNONE,

    NM_ITEM,  "Log View",   NULL,  0L,          NULL, NULL,
    NM_SUB,   "Off",          "1",  CHECKIT,     NULL, (void *) O_LOGVIEWOFF,
    NM_SUB,   "25%",          "2",  CHECKIT,     NULL, (void *) O_LOGVIEW25,
    NM_SUB,   "50%",          "3",  CHECKIT,     NULL, (void *) O_LOGVIEW50,
    NM_SUB,   "75%",          "4",  CHECKIT,     NULL, (void *) O_LOGVIEW75,
    NM_SUB,   "Full",         "5",  CHECKIT,     NULL, (void *) O_LOGVIEWFULL,

    NM_ITEM,  "Revert Comment",".", 0L,          NULL, (void *) O_REVERTCOMMENT,
    NM_ITEM,  "Clear Log View","Z", 0L,          NULL, (void *) O_CLEARLOGVIEW,
    
    NM_END,   NULL, 		 NULL, 	NULL, 	   	 NULL, NULL
};

/* GUI functions */
struct WindowStuff * SetupTrackWindow(struct WindowStuff *);
void HandleIDCMP(struct WindowStuff *);
BOOL UpdateWindow(struct WindowStuff * win, BOOL BFree);
struct Node * AllocDisplayItem(char * szHostName, ULONG ulIPAddress, ULONG ulHostFieldLen, char * szComment);
void AttachClientList(struct WindowStuff * win, BOOL BAttach);
void AttachLogList(struct WindowStuff * win, BOOL BAttach);
BOOL CreateTrackMenus(struct WindowStuff * win, BOOL BCreate);

/* ------------ Amiga GUI functions -------------------------- */

#define HSPACE               5
#define VSPACE               5
#define SERVER_STRING_TEXT   "Server:"
#define PORT_STRING_TEXT     "Port:"
#define COMMENT_STRING_TEXT  "Comment:"

void edebug(int i)
{
  int t = *((int *)i);
  t++;
}

struct String * NewString(char * string)
{
  struct String * ret;
  ULONG len = strlen(string)+1;
  
  if (ret = AllocMem(sizeof(struct String) + len, MEMF_ANY))
  {
    ret->bufLen = len;
    ret->buffer = &ret[1];
    strcpy(ret->buffer, string);
  }
  return(ret);
};

int GetLogViewLevelPercent(ULONG ulCode)
{
  switch(ulCode)
  {
    case O_LOGVIEWOFF:   return 0;
    case O_LOGVIEW25:    return 25;
    case O_LOGVIEW50:    return 50;
    case O_LOGVIEW75:    return 75;
    case O_LOGVIEWFULL:  return 100;
  }
  return -1;  /* error! */
}

void FreeString(struct String * s)
{
  FreeMem(s, sizeof(struct String) + s->bufLen);
}

/* Returns TRUE and sets s on success, FALSE on failure.
   On failure, old string is still okay. */
BOOL SetString(struct String ** s, char * newVal)
{
  struct String * newString;
  
  UNLESS((newVal)&&(newString = NewString(newVal))) return(FALSE);
  FreeString(*s);
  *s = newString;
  return(TRUE);
}

int GetFieldIDByName(char * name)
{
  UNLESS(strcmp(name,"comment"))  return(O_COMMENT);
  UNLESS(strcmp(name,"hostname")) return(O_VIEWHOSTNAME);
  UNLESS(strcmp(name,"username")) return(O_VIEWUSERNAME);
  UNLESS(strcmp(name,"realname")) return(O_VIEWREALNAME);
  return(0);
}

struct String ** getField(struct Client * c, char * f)
{
  UNLESS(strcmp(f,"comment"))  return(&c->comment);
  UNLESS(strcmp(f,"username")) return(&c->userName);
  UNLESS(strcmp(f,"realname")) return(&c->realName);
  UNLESS(strcmp(f,"winopen"))  return(&c->winOpen);
  return(NULL);
}


void FreeClient(struct Client * c)
{
  if (c->hostName)   FreeString(c->hostName);
  if (c->userName)   FreeString(c->userName);
  if (c->realName)   FreeString(c->realName);
  if (c->comment)    FreeString(c->comment);
  if (c->listString) FreeString(c->listString);
  if (c->winOpen)    FreeString(c->winOpen);
  if (cLastEntryClicked == c) cLastEntryClicked = NULL;
  FreeMem(c, sizeof(struct Client));
}

struct Client * NewClient(char * hostName)
{
  struct Client * ret;
  
  if (ret = AllocMem(sizeof(struct Client), MEMF_CLEAR))
  {
    if ((ret->firstTime  = TRUE) &&
        (ret->hostName   = NewString(hostName))&&
        (ret->userName   = NewString("anon"))&&
        (ret->realName   = NewString("Anonymous"))&& /*********************/
        (ret->winOpen    = NewString(""))&&          /* Will be set later */
        (ret->comment    = NewString(""))&&          /*********************/
        (ret->listString = NewString("")))
    {
      ret->node.ln_Name = "";
      ret->timeStamp  = time(NULL);
      ret->viewOffset = 0;
    }
    else
    {
      /* Failure! */
      FreeClient(ret);
      ret = NULL;
    }      
  }
  return(ret);
}

/* To tear down, set BCreate==FALSE */
BOOL CreateTrackMenus(struct WindowStuff * win, BOOL BCreate)
{   
	UNLESS((win)&&(win->vi)) return(FALSE);
	
	UNLESS(BCreate) 
	{
		if (Menu) 
		{
			ResetMenuStrip(win->win,Menu); 
			FreeMenus(Menu); 
			Menu = NULL;
		}
		return(FALSE);
	}

	/* Create menus */
	UNLESS((Menu = CreateMenus(nmMenus, TAG_DONE)) &&
	       (LayoutMenus(Menu, win->vi, TAG_DONE)))
	       return(CreateTrackMenus(win, FALSE));

	SetMenuStrip(win->win, Menu);
	return(TRUE);
}

struct String * getDisplayIndexString(struct Client * c)
{
  switch(nViewBy)
  {
    case O_VIEWHOSTNAME: return c->hostName; 
    case O_VIEWUSERNAME: return c->userName; 
    case O_VIEWREALNAME: return c->realName; 
  }
  printf("getDisplayIndexString:  warning, returning NULL!\n");
  return(NULL);
}

void dateStamp(FILE * f)
{
  static char temp[100], *t;
  
  sprintf(temp, "%s", ctime(NULL));
  if (t = strchr(temp, '\n')) *t = '\0';
  fprintf(f, "%s ",temp);
}

void RunRexxScriptsForEvent(int opCode, struct Client * actOn, char * fieldChanged, char * aux1, char * aux2)
{
  int updateType = fieldChanged ? GetFieldIDByName(fieldChanged) : 0;
  char * rexxScript = rexxScripts[opCode];

  if ((opCode == LOG_UPDATE)&&(updateType != O_COMMENT)) return;  /* Non-comment types are redundant */
  if (*rexxScript == '\0') rexxScript = rexxScripts[LOG_DEFAULT];
  if (*rexxScript)
  {
    static char temp[256];
    char sep[] = "^";
    
    Strncpy(temp, rexxScript, sizeof(temp));
    Strncat(temp, " ", sizeof(temp));
    Strncat(temp, rexxScriptNames[opCode], sizeof(temp));
    
    switch(opCode)
    {
      case LOG_UPDATE:
      case LOG_LOGON:
      case LOG_LOGOFF:
        Strncat(temp, sep, sizeof(temp));
        Strncat(temp, actOn->hostName->buffer, sizeof(temp));
        Strncat(temp, sep, sizeof(temp));
        Strncat(temp, actOn->userName->buffer, sizeof(temp));
        Strncat(temp, sep, sizeof(temp));
        Strncat(temp, actOn->realName->buffer, sizeof(temp));
        Strncat(temp, sep, sizeof(temp));
        Strncat(temp, (*actOn->winOpen->buffer == 'Y') ? "Y" : "N", sizeof(temp));
        Strncat(temp, sep, sizeof(temp));
        Strncat(temp, actOn->comment->buffer, sizeof(temp));
      break;

      case LOG_CONNECT:
      case LOG_DISCONNECT:
        if (szServerName) 
        {
          Strncat(temp, sep, sizeof(temp));
          Strncat(temp, szServerName, sizeof(temp));
        }
      break;

      case LOG_SYSMESSAGE:
        Strncat(temp, sep,  sizeof(temp));
        Strncat(temp, aux1, sizeof(temp));
        Strncat(temp, sep,  sizeof(temp));
        Strncat(temp, aux2, sizeof(temp));
      break;
      
      case LOG_START:
      case LOG_END:
        /* No info to append */
    }

    if (rexxHost) SendRexxCommand(rexxHost, temp, 0L);
  }
}


void LogEvent(int opCode, struct Client * actOn, char * fieldChanged, char * aux1, char * aux2)
{
  FILE * logfile;
  char * id      = actOn ? getDisplayIndexString(actOn)->buffer : NULL;
  char * comment = actOn ? actOn->comment->buffer       : NULL;
  int updateType = fieldChanged ? GetFieldIDByName(fieldChanged) : 0;
  
  if ((id)&&(*id == '\0')) id = actOn->hostName->buffer;

  if (nLogLevel == OUTPUT_NONE) return;
  UNLESS((szLogFileName)&&(*szLogFileName)&&(logfile = fopen(szLogFileName, "a"))) return;

  switch(nLogLevel)
  {
    case OUTPUT_VERBOSE:
     if (opCode == LOG_START) {dateStamp(logfile); fprintf(logfile, "QAmiTrack begins execution.\n");}
     if (opCode == LOG_END)   {dateStamp(logfile); fprintf(logfile, "QAmiTrack exits.\n");}
     
    case OUTPUT_MODERATE:
     if ((opCode == LOG_UPDATE)&&(updateType == O_COMMENT)&&(*comment)) {dateStamp(logfile); fprintf(logfile, "[%s] says: [%s]\n", id, comment);}
     if (opCode == LOG_CONNECT){dateStamp(logfile); fprintf(logfile, "QAmiTrack has connected to server [%s]\n", szServerName);}
     
    case OUTPUT_TERSE: 
     if ((BConnected)&&(opCode == LOG_DISCONNECT)) 
     {
       dateStamp(logfile); 
       fprintf(logfile,"The QAmiTrack connection was closed%s%s%s.\n",szServerName,aux1?" [":"",aux1?aux1:"",aux1?"]":"");
     }  
     if (opCode == LOG_LOGON)      
     {
       dateStamp(logfile); fprintf(logfile, "[%s] is now online.\n",id);
     }
     if (opCode == LOG_LOGOFF)     {dateStamp(logfile); fprintf(logfile, "[%s] is no longer on line.\n",id);}
     if (opCode == LOG_SYSMESSAGE) {dateStamp(logfile); fprintf(logfile, "System Message from [%s]: [%s]\n", aux1, aux2);}

    case OUTPUT_NONE: 
     /* Do nothing--actually this case is never executed anyway  */
  }
  fclose(logfile);
}


void LogDisplay(char * msg)
{
  struct Node * node = AllocMem(sizeof(struct Node)+strlen(msg)+1, MEMF_CLEAR);

  if (node)
  {
    node->ln_Name = ((char *)node)+sizeof(struct Node);
    strcpy(node->ln_Name, msg);
    AttachLogList(TrackWindow, FALSE);
    AddTail(&logList, node); logListLen++;
    if (logListLen > maxLogListLen)
    {
      struct Node * top = RemHead(&logList);
      if (top) 
      {
        FreeMem(top,sizeof(struct Node)+strlen(top->ln_Name)+1);
        logListLen--;
      }
    }
    AttachLogList(TrackWindow, TRUE);
    if ((TrackWindow)&&(TrackWindow->win)&&(TrackWindow->LogListGadget)) 
       GT_SetGadgetAttrs(TrackWindow->LogListGadget, TrackWindow->win, NULL, GTLV_Top, logListLen+1, TAG_END);       
       
    if (BBeepOnLog) DisplayBeep(NULL);
  }
  else printf("Oops, no memory to allocate Log Display item!\n");
}

void ClearLogView(void)
{
  struct Node * next;
 
  AttachLogList(TrackWindow, FALSE);
  while(next = RemHead(&logList)) FreeMem(next,sizeof(struct Node)+strlen(next->ln_Name)+1);
  logListLen = 0;
  AttachLogList(TrackWindow, TRUE);
}

void DisplayEvent(int opCode, struct Client * actOn, char * fieldChanged, char * aux1, char * aux2)
{
  char * id      = actOn ? getDisplayIndexString(actOn)->buffer : NULL;
  char * comment = actOn ? actOn->comment->buffer       : NULL;
  int updateType = fieldChanged ? GetFieldIDByName(fieldChanged) : 0;
  static char temp[512];
    
  if ((id)&&(*id == '\0')) id = actOn->hostName->buffer;

  switch(nLogLevel)
  {
    case OUTPUT_VERBOSE:
     if (opCode == LOG_START) LogDisplay("(QAmiTrack begins execution)");
     if (opCode == LOG_END)   LogDisplay("(QAmiTrack exits)");
     
    case OUTPUT_MODERATE:
     if (opCode == LOG_CONNECT)
     {
       Strncpy(temp, "QAmiTrack has connected to server [", sizeof(temp));
       Strncat(temp, szServerName, sizeof(temp));
       Strncat(temp, "]", sizeof(temp));
       LogDisplay(temp);
     }
     if (opCode == LOG_LOGON)      
     {
       Strncpy(temp, "[", sizeof(temp));
       Strncat(temp, id, sizeof(temp));
       Strncat(temp, "] is now online.", sizeof(temp));
       LogDisplay(temp);
     }
     if (opCode == LOG_LOGOFF)     
     {
       Strncpy(temp, "[", sizeof(temp));
       Strncat(temp, id, sizeof(temp));
       Strncat(temp, "] is no longer online.", sizeof(temp));
       LogDisplay(temp);
     }

    case OUTPUT_TERSE: 
     if ((BConnected)&&(opCode == LOG_DISCONNECT)) 
     {
       Strncpy(temp, "The QAmiTrack connection was closed.", sizeof(temp));
       if (aux1)
       {
         Strncat(temp, " [", sizeof(temp));
         Strncat(temp, aux1, sizeof(temp));
         Strncat(temp, "]", sizeof(temp));
       }
       LogDisplay(temp);
     }  
     if ((opCode == LOG_UPDATE)&&(updateType == O_COMMENT)&&(*comment))
     {
       Strncpy(temp, id, sizeof(temp));
       Strncat(temp, ": ", sizeof(temp));
       Strncat(temp, comment, sizeof(temp));
       LogDisplay(temp);
     }


    case OUTPUT_NONE: 
     if (opCode == LOG_SYSMESSAGE) 
     {
       Strncpy(temp, "SYSTEM MESSAGE from [", sizeof(temp));
       Strncat(temp, aux1, sizeof(temp));
       Strncat(temp, "]: ", sizeof(temp));
       Strncat(temp, aux2, sizeof(temp));
       LogDisplay(temp);
     }
  }
}

void RecordEvent(int opCode, struct Client * actOn, char * fieldChanged, char * aux1, char * aux2)
{
  LogEvent(opCode, actOn, fieldChanged, aux1, aux2);
  DisplayEvent(opCode, actOn, fieldChanged, aux1, aux2);
  RunRexxScriptsForEvent(opCode, actOn, fieldChanged, aux1, aux2);
}

void InitActions()
{
  int i;
  
  for (i=0; i<(sizeof(Actions)/sizeof(char *)); i++)
    Actions[i] = ActionLabels[i] = NULL;
  nNumActions = 0;
}

void FreeActions()
{
  int i;
  
  for (i=0; i<(sizeof(Actions)/sizeof(char *)); i++)
  {
    if (Actions[i]) ReplaceAllocedString(&Actions[i], NULL);
    if (ActionLabels[i]) ReplaceAllocedString(&ActionLabels[i], NULL);
  }
  nNumActions = 0;
}


struct Client * GetClientByIndex(int index)
{
  struct Client * next = (struct Client *) clientList.lh_Head;
  
  while(index--) UNLESS(next = next->node.ln_Succ) return(NULL);
  return(next);
}

void ListClicked(int index)
{
  static time_t tLastTimeClicked = (time_t) -1;
  time_t tPreviousTimeClicked = tLastTimeClicked;
	
  tLastTimeClicked = time(NULL);

  /* Record which client it was */
  nLastEntryClicked = index;  
  cLastEntryClicked = GetClientByIndex(nLastEntryClicked);
    	
  if ((index == nLastEntryClicked) &&
     ((tLastTimeClicked-tPreviousTimeClicked) < 2)) (void)DoAction(index, currentAction, BConfirmAppLaunch);		
}


struct Client * GetHost(int index)
{
  struct Client * next = (struct Client *) clientList.lh_Head;
  
  while(index--) {UNLESS(next = next->node.ln_Succ) return(NULL);}
  return(next);
}

int GetTrackHostByName(char * name)
{
  struct Node * current;
  int i=0;
  
  current = clientList.lh_Head;
  while(current->ln_Succ)
  {
    struct Client * c = (struct Client *) current;
    
    if (strcmp(c->hostName->buffer, name) == 0) return(i);
    current = current->ln_Succ;
    i++;
  }
  return(-1);
}

int GetTrackActionByName(char * name)
{
  int i;
  
  for (i=0;i<nNumActions;i++) if (strcmp(name, ActionLabels[i])==0) return(i);
  return(-1);
}

/* Allocates a newly malloc'd string with all the replaceMe's replaced with withMe's */
/* free()'s oldString, or returns it on error or if no changes were made. */
char * makeSubbedString(char * oldString, char * replaceMe, char * withMe)
{
  int len = strlen(oldString);
  int oldStringLen = len;
  int replaceMeLen = strlen(replaceMe);
  int withMeLen    = strlen(withMe);
  int diff = withMeLen-replaceMeLen;
  int numChanges = 0;
  char * newString, * temp = oldString;
      
  /* Figure out how much extra space we'll need */
  while(temp = strstr(temp, replaceMe))
  {
    len += diff;
    temp += replaceMeLen;
    numChanges++;
  }
  if (numChanges == 0) return(oldString);
  if (newString = malloc(len+1+1))  /* 1 for the NUL, 1 for the debug guard */
  {
    char * nextSub;

    temp = oldString;    
    *newString = '\0';

    /* Place the debug guard here! */
    newString[len+1] = '~';  /* After the NUL, we'll check later to see if this was overridden (which would indicate a BUG!) */
    while(nextSub = strstr(temp, replaceMe))
    {      
      char c = *nextSub;
      
      *nextSub = '\0';
      strcat(newString, temp);
      strcat(newString, withMe);
      temp = nextSub + replaceMeLen;
      *nextSub = c;
    }
    strcat(newString, temp);  
    
    if (newString[len+1] != '~') (void)MakeReq(NULL, "There's a BUG in QAmiTrack's launching code!", "Better go email Jeremy!");
    free(oldString);
  }
  return(newString ? newString : oldString);
}

int DoAction(int index, int action, BOOL BConf)
{
  int result = -2;
  struct Client * target;
  char * s;
  struct Task * thisTask = FindTask(NULL);
  int stackSize = thisTask->tc_SPUpper - thisTask->tc_SPLower;
  
printf("stackSize = %i\n",stackSize);
  UNLESS((action < nNumActions)&&(action >= 0)&&(target = GetHost(index))) return(-1);
  
  if (BConf)
  {
    /* Do user confirmation */
    const char p1[] = "Okay to connect to ";
    const char p2[] = " using ";
    const char p3[] = "?";
    char * reqText = NULL;
    int reqTextLen = strlen(p1) + strlen(target->hostName->buffer) + strlen(p2) + strlen(ActionLabels[action]) + strlen(p3) + 1;
    BOOL BOk;

    UNLESS(reqText = AllocMem(reqTextLen, MEMF_ANY)) return(-1);
    sprintf(reqText, "%s%s%s%s%s",p1,target->hostName->buffer,p2,ActionLabels[action],p3);
    BOk = MakeReq("QAmiTrack Launch Confirmation", reqText, "Connect|Cancel");
    FreeMem(reqText, reqTextLen);
    UNLESS(BOk) return(-2);
  }
    
  if (s = strdup(Actions[action]))
  {
    char timeString1[35], timeString2[35];
      
    sprintf(timeString1,"%i",(time(NULL)-target->timeStamp)/timeUnit);
    sprintf(timeString2,"%i",(time(NULL)-lastUpdateTime)/timeUnit);

    s = makeSubbedString(s, "%s", target->hostName->buffer);      
    s = makeSubbedString(s, "%h", target->hostName->buffer);
      
    s = makeSubbedString(s, "%u", target->userName->buffer);
    s = makeSubbedString(s, "%U", myInfo->userName->buffer);
      
    s = makeSubbedString(s, "%c", target->comment->buffer);
    s = makeSubbedString(s, "%C", myInfo->comment->buffer);
    
    s = makeSubbedString(s, "%r", target->realName->buffer);
    s = makeSubbedString(s, "%R", myInfo->realName->buffer);
      
    s = makeSubbedString(s, "%l", target->listString->buffer);
      
    s = makeSubbedString(s, "%t", timeString1);
    s = makeSubbedString(s, "%T", timeString2);

    s = makeSubbedString(s, "%%", "%");
    s = makeSubbedString(s, "%q", "\"");

    if ((result = SystemTags(s, 
       SYS_Asynch, TRUE,
       SYS_Input,  Open("NIL:",MODE_OLDFILE), 
       SYS_Output, NULL,
       NP_StackSize, stackSize, 
       TAG_DONE)) == -1)
         MakeReq(NULL,"Error launching program!", NULL);

    free(s);
  }
  return(result);
}


int getActionCycleWidth(struct WindowStuff * win)
{	
  int i,len = 0;
  for (i=0; (ActionLabels[i] != NULL); i++)
  {
    int next = TextLength(&win->screen->RastPort, ActionLabels[i], strlen(ActionLabels[i]));
    if (next > len) len = next; 
  }
  if (len > 0) len += HSPACE*6; /* For the arrow icon! */
  if (len > (win->win->Width/2)) len = win->win->Width/2;  /* Not TOO wide! */
  return(len);
}
	

/* Updates/reupdates the window on creation or after a size change */
/* If BFree is TRUE, just deallocate anything that was allocated */
/* Returns the allocated/deallocated state of the window gadgets */
BOOL UpdateWindow(struct WindowStuff * win, BOOL BFree)
{
	struct NewGadget ng;
	struct Gadget * gad;
	int actionLabelLength;
    int leftoverHeight, commentListViewHeight, logListViewHeight;

	UNLESS((win)&&(win->screen)) return(FALSE);
	
	/* First deallocate */
	if ((win->win)&&(win->glist)) RemoveGList(win->win, win->glist, -1);
	if (win->glist)	{FreeGadgets(win->glist); win->glist = NULL;}

	/* Mark all gadgets as free */
	win->CommentListGadget = win->LogListGadget = 
	win->ServerString = win->PortString = win->CommentString = NULL;

	if (Menu) CreateTrackMenus(win, FALSE);
	if (win->vi) {FreeVisualInfo(win->vi); win->vi = NULL;}
	if (BFree) return(FALSE);	/* If we're just freeing, that's all to do */

	if (win->win)
	{
		/* erase any gadget imagery */
		EraseRect(win->win->RPort,win->win->BorderLeft, win->win->BorderTop,
		  win->win->Width  - win->win->BorderRight - 1,
          win->win->Height - win->win->BorderBottom - 1);
		RefreshWindowFrame(win->win);   			
	}

    /* Make sure datestamps are up to date and timer is ticking */
    UpdateWindowTimes();

	/* Make everything NULL, etc. */
	memset(&ng, 0, sizeof(ng));

	/* Now start allocating */
	UNLESS(win->vi = GetVisualInfo(win->screen,TAG_END)) return(FALSE);
	UNLESS(CreateTrackMenus(win, TRUE))                  return(FALSE);
	UNLESS(gad     = CreateContext(&win->glist))         return(FALSE);

	/* Allocate Server gadget */
	ng.ng_VisualInfo = win->vi;
	ng.ng_TextAttr   = &win->font;
	ng.ng_Height     = win->font.ta_YSize + VSPACE;
	ng.ng_GadgetText = SERVER_STRING_TEXT;
	ng.ng_LeftEdge   += win->screen->WBorLeft + TextLength(&win->screen->RastPort, ng.ng_GadgetText, strlen(ng.ng_GadgetText)) + (HSPACE*3);
	ng.ng_TopEdge    = win->screen->WBorTop + win->screen->RastPort.TxHeight + VSPACE;
	ng.ng_Width      = ((win->win->Width * 2)/3) - ng.ng_LeftEdge;
	ng.ng_GadgetID   = GAD_SERVERSTRING;
	ng.ng_Flags	     = PLACETEXT_LEFT;
	win->ServerString = gad = CreateGadget(STRING_KIND, gad, &ng, GTST_String, szServerName, GT_Underscore, '_', TAG_END);

	/* Allocate Port gadget */
	ng.ng_GadgetText = PORT_STRING_TEXT;
	ng.ng_LeftEdge   += ng.ng_Width + TextLength(&win->screen->RastPort, ng.ng_GadgetText, strlen(ng.ng_GadgetText)) + (HSPACE*3);
	ng.ng_Width      = win->win->Width - ng.ng_LeftEdge - win->screen->WBorRight - HSPACE;
	ng.ng_GadgetID   = GAD_PORTSTRING;
	ng.ng_Flags	     = PLACETEXT_LEFT;
	win->PortString = gad = CreateGadget(INTEGER_KIND, gad, &ng, GTIN_Number, nPort, GT_Underscore, '_', TAG_END);

	/* Calculate the width of the widest action label */
	actionLabelLength = getActionCycleWidth(win);
	
	/* Allocate Comment gadget */
	ng.ng_GadgetText = COMMENT_STRING_TEXT;
	ng.ng_LeftEdge   = win->screen->WBorLeft + TextLength(&win->screen->RastPort, ng.ng_GadgetText, strlen(ng.ng_GadgetText)) + (HSPACE*3);
	ng.ng_TopEdge    += ng.ng_Height + VSPACE;
	ng.ng_Width      = win->win->Width - ng.ng_LeftEdge - win->screen->WBorRight - HSPACE - actionLabelLength - (actionLabelLength ? 2*HSPACE : 0);
	ng.ng_GadgetID   = GAD_COMMENTSTRING;
	ng.ng_Flags	     = PLACETEXT_LEFT;
	win->CommentString = gad = CreateGadget(STRING_KIND, gad, &ng, GTST_String, myInfo->comment->buffer, GTST_MaxChars, 1023, GT_Underscore, '_', TAG_END);

    /* Allocate Actions cycle, if we have any actions! */
    if (actionLabelLength > 0)
    {
	  ng.ng_GadgetText = NULL;
	  ng.ng_LeftEdge   += ng.ng_Width + HSPACE;
	  ng.ng_Width      = actionLabelLength + HSPACE;
	  ng.ng_GadgetID   = GAD_ACTIONCYCLE;
	  win->ActionCycle = gad = CreateGadget(CYCLE_KIND, gad, &ng, GTCY_Labels, ActionLabels, GTCY_Active, currentAction, GT_Underscore, '_', TAG_END);
    }
    else win->ActionCycle = NULL;

	/* Allocate ListView */
	ng.ng_GadgetText = NULL;
	ng.ng_TextAttr   = &win->fixedfont;
	ng.ng_LeftEdge   = win->screen->WBorLeft + HSPACE;
	ng.ng_TopEdge   += ng.ng_Height + VSPACE;
	ng.ng_Width	     = win->win->Width - ng.ng_LeftEdge - win->screen->WBorRight - HSPACE;

    /* calculate heights of the two ListView's */
    leftoverHeight = win->win->Height - ng.ng_TopEdge - win->screen->WBorBottom - (VSPACE*2);
    switch(nLogViewLevel)
    {
       case O_LOGVIEWOFF:  commentListViewHeight = leftoverHeight;       logListViewHeight = 0;                    break;
       case O_LOGVIEW25:   commentListViewHeight = (3*leftoverHeight)/4; logListViewHeight = leftoverHeight/4;     break;
       case O_LOGVIEW50:   commentListViewHeight = leftoverHeight/2;     logListViewHeight = leftoverHeight/2;     break;
       case O_LOGVIEW75:   commentListViewHeight = leftoverHeight/4;     logListViewHeight = (3*leftoverHeight)/4; break;
       case O_LOGVIEWFULL: commentListViewHeight = 0;                    logListViewHeight = leftoverHeight;       break;      
    }

	ng.ng_Height     = commentListViewHeight;
	ng.ng_GadgetID   = GAD_CLIENTLIST;

	if (ng.ng_Height > 0)
	{
	   struct Gadget * tempGad = win->CommentListGadget = CreateGadget(LISTVIEW_KIND, gad, &ng, GTLV_Labels, &clientList, GTLV_ReadOnly, FALSE, TAG_END);
	   if (tempGad) gad = tempGad;
	   ng.ng_TopEdge += ng.ng_Height;
	}
	
	ng.ng_Height     = logListViewHeight;
	ng.ng_GadgetID   = GAD_LOGLIST;
	if (ng.ng_Height > 0)
	{
	   struct Gadget * tempGad = win->LogListGadget = CreateGadget(LISTVIEW_KIND, gad, &ng, GTLV_Labels, &logList, GTLV_ReadOnly, TRUE, TAG_END);
	   if (tempGad) gad = tempGad;
	   ng.ng_TopEdge += ng.ng_Height;
	}

	/* Attach gadgets to window */
	AddGList(win->win, win->glist, -1, -1, NULL);
	RefreshGList(win->glist, win->win, NULL, -1);
	GT_RefreshWindow(win->win, NULL);

    if ((win)&&(win->win)&&(win->LogListGadget)) 
       GT_SetGadgetAttrs(win->LogListGadget, win->win, NULL, GTLV_Top, logListLen+1, TAG_END);

	return(TRUE);
}


void AttachClientList(struct WindowStuff * win, BOOL BAttach)
{
  if ((win)&&(win->win)&&(win->CommentListGadget)) 
  {
    GT_SetGadgetAttrs(win->CommentListGadget, win->win, NULL, GTLV_Labels, (BAttach) ? (&clientList) : ((struct List *)(~0)), TAG_END);
  }
}

void AttachLogList(struct WindowStuff * win, BOOL BAttach)
{
  if ((win)&&(win->win)&&(win->LogListGadget)) 
  {
    GT_SetGadgetAttrs(win->LogListGadget, win->win, NULL, GTLV_Labels, (BAttach) ? (&logList) : ((struct List *)(~0)), TAG_END);
  }
}

int GetNumItems(struct List * list)
{
  int count = 0;
  struct Node * current = list->lh_Head;

  while(current->ln_Succ)
  {
    count++;
    current = current->ln_Succ;
  }
  return(count);
}

BOOL SendState(ULONG which)
{
  if (session)
  {    
    lastUpdateTime = time(NULL);  /* Mark now as the last time we were changed */
    
    /* concatenate the data items together */
    if (which & STATE_REALNAME) QSetOp(session, "realname", myInfo->realName->buffer, myInfo->realName->bufLen);
    if (which & STATE_USERNAME) QSetOp(session, "username", myInfo->userName->buffer, myInfo->userName->bufLen);
    if (which & STATE_WINOPEN)  QSetOp(session, "winopen",  myInfo->winOpen->buffer, myInfo->winOpen->bufLen);
    if (which & STATE_COMMENT)  QSetOp(session, "comment",  myInfo->comment->buffer, myInfo->comment->bufLen);

	QGo(session, 0L);
  }
  return(session != NULL);
}

void SetCommentString(struct WindowStuff * win, char * newVal, BOOL BFinal)
{			              	
  /* Grab and sanitize the new comment */
  SetString(&myInfo->comment, PastSpaces(RemoveUnprintableChars(newVal)));
  if (win) GT_SetGadgetAttrs(win->CommentString, win->win, NULL, GTST_String, myInfo->comment->buffer, TAG_END);
  if (BFinal) SendState(STATE_COMMENT);
}

/* Frees a malloced() and strduped() Null-terminated string vector */
void FreeStringArray(char ** array)
{
  char ** next = array, * This;
  
  while(This = *next) {free(This); next++;}
  free(array);
}

void SetServerString(struct WindowStuff * win, char * newVal)
{
  char * szTemp = NULL;
			              	
  /* Grab and sanitize the new server string */
  ReplaceAllocedString(&szTemp, newVal);
  ReplaceAllocedString(&szServerName, ToLower(PastSpaces(RemoveUnprintableChars(szTemp))));
  ReplaceAllocedString(&szTemp, NULL);
  if (win) GT_SetGadgetAttrs(win->ServerString, win->win, NULL, GTST_String, szServerName, TAG_END);
  SetTrackFlag(CODE_RECONNECT);
}

void SetPortNumber(struct WindowStuff * win, int nP)
{
  if (nP <= 0) nP = DEFAULT_AMITRACK_PORT;
  if (win) GT_SetGadgetAttrs(win->PortString, win->win, NULL, GTIN_Number, nP, TAG_END);
  SetTrackFlag(CODE_RECONNECT);
  nPort = nP;
}


void SetCurrentAction(struct WindowStuff * win, char * name)
{
  int i;
  
  for (i=0; i<nNumActions; i++)
  {
    if (strcmp(name, ActionLabels[i]) == 0)
    {
      SetCurrentActionIndex(win, i);
      return;
    }
  }
}

void SetCurrentActionIndex(struct WindowStuff * win, int code)
{
  currentAction = (code >= nNumActions) ? nNumActions-1 : code; 
  if ((win)&&(currentAction >= 0)) 
    GT_SetGadgetAttrs(win->ActionCycle, win->win, NULL, 
       GTCY_Active, currentAction, 
       GTCY_Labels, ActionLabels,
       TAG_END);
}

struct Client * FindClientByName(char * hostName)
{
  struct Client * c = (struct Client *)clientList.lh_Head;
  
  while(c->node.ln_Succ)
  {
    UNLESS(strcmp(hostName, c->hostName->buffer)) return(c);
    c = (struct Client *)c->node.ln_Succ;
  }
  return(NULL);
}


BOOL OutOfOrder(struct Client * c1, struct Client * c2)
{
  switch(nSortBy)
  {
    case O_SORTBYAGE:     return(c1->timeStamp<c2->timeStamp); 
    case O_SORTBYNAME:    return(stricmp(getDisplayIndexString(c1)->buffer,getDisplayIndexString(c2)->buffer)>0); 
    case O_SORTBYCOMMENT: return(stricmp(c1->comment->buffer,c2->comment->buffer)>0); 
    default:              return(FALSE);
  }
}

void SortClientList(void)
{
  int nSwappedThisPass = 1;
  struct Node *current, *past;

  if ((clientListLength <= 1)||(nSortBy == O_SORTBYNONE)) return;
  
  while (nSwappedThisPass > 0)
  {
    /* start a pass */
    past = clientList.lh_Head;
    current = past ? past->ln_Succ : NULL;
    nSwappedThisPass = 0;		/* start with none recorded */

    while ((past)&&(current)&&(current->ln_Succ))
    {
      if (OutOfOrder((struct Client *)past, (struct Client *)current))
      {
        /* Swap the two nodes */
        Remove(past);
        Insert(&clientList, past, current);
        nSwappedThisPass++;
      }

      past = current;
      current = current->ln_Succ;
    }
  }
}

struct Client * AddClient(char * hostName)
{
  struct Client * newClient;

  if (newClient = NewClient(hostName))
  {
    /* Append now node to the clientList */
    struct Client * next = (struct Client *)clientList.lh_Head;
    
    AddTail(&clientList, (struct Node *)newClient);
    clientListLength++;
  }
  return(newClient);
}

struct String * MakeNewDisplayString(struct Client * c, int nameCol)
{
  const int dateLen = sizeof("00");
  int stringLen;
  struct String * s, * iString = getDisplayIndexString(c);
  
  if (BRaggedText) nameCol = iString->bufLen;
  
  nameCol += dateLen;  /* Prepending chars for the time elapsed! */
  stringLen = nameCol + c->comment->bufLen;
  
  if (s = AllocMem(sizeof(struct String)+stringLen, MEMF_ANY))
  {    
    if (c->viewOffset >= (c->comment->bufLen-1)) c->viewOffset = c->comment->bufLen-2;
    if (c->viewOffset < 0)                       c->viewOffset = 0;

    time_t timeDiff = time(NULL)-(c->timeStamp);
    s->bufLen = stringLen;
    s->buffer = (char *)(&s[1]);
    memset(s->buffer, ' ', stringLen);
    
    /* Only display time if it fits! */
    if ((timeDiff/timeUnit) < 100) sprintf(s->buffer,"%2i", timeDiff/timeUnit);
                              else sprintf(s->buffer,"  ");
    s->buffer[2] = (c->winOpen->buffer[0]=='Y') ? '+' : ' ';
    strcpy(s->buffer+dateLen,iString->buffer);
    *(s->buffer+dateLen+iString->bufLen-1)=' ';
    strcpy(s->buffer+nameCol,c->comment->buffer+c->viewOffset); 
  }
  return(s);
}

/* Go through displayed items, reprinting the current elapsed time in each.  Reset timer when done! */
void UpdateWindowTimes(void)
{
  struct Client * next;
  time_t currentTime = time(NULL);
  
  AttachClientList(TrackWindow, FALSE);
  
  next = (struct Client *)clientList.lh_Head;
  while(next->node.ln_Succ)
  {
    char * buf = next->listString->buffer;
    
    if ((buf)&&(buf[0])&&(buf[1])&&(buf[2])&&(buf[3]))
    {
      time_t timeDiff = currentTime - next->timeStamp;
      
      if ((timeDiff/timeUnit) < 100) sprintf(buf,"%2i", timeDiff/timeUnit);
                                else sprintf(buf,"  ");
      buf[2] = (next->winOpen->buffer[0]=='Y') ? '+' : ' ';
    }
    next = (struct Client *)next->node.ln_Succ;
  }
  
  SortClientList();
  AttachClientList(TrackWindow, TRUE);
}

BOOL UpdateClientTime(char * path, ULONG secs)
{
  char * slash;
  
  path++; 
  if (slash = strchr(path, '/')) 
  {
    struct Client * client;
    
    *slash = '\0';
    if (client = FindClientByName(path))
    {
      client->timeStamp = time(NULL)-secs;
      return(TRUE);
    }
  }
  return(FALSE);
}

void UpdateClientField(struct Client * modme, char * fieldName, char * newVal)
{        
  struct String ** stringToChange;
  BOOL BLogIt = TRUE;
  
  if (stringToChange = getField(modme, fieldName))
  {
    /* If the new string is the same as the old, no need to update anything! */
    BLogIt = !((*stringToChange)&&((*stringToChange)->buffer)&&
               (newVal)&&(strcmp((*stringToChange)->buffer, newVal) == 0));

    SetString(stringToChange, newVal);
    SetString(&modme->listString, "");  /* force update */
  }
  modme->timeStamp  = time(NULL);
  modme->viewOffset = 0;

  if ((modme->firstTime)&&(GetFieldIDByName(fieldName)==O_COMMENT))
  {
    if (BLogIt) RecordEvent(LOG_LOGON, modme, fieldName, NULL, NULL);
    modme->firstTime=FALSE;
  }
  else if (BLogIt) RecordEvent(LOG_UPDATE, modme, fieldName, NULL, NULL);
}

void UpdateClientList(struct QMessage * qmsg)
{
  /* Parse path */
  if (qmsg)
  {
    char * path = qmsg->qm_Path;
    char * data = qmsg->qm_Data;
    char * slash1=NULL, * slash2=NULL;
    char * hostName, * field;
    struct Client * modme;
    
    UNLESS((path)&&(slash1 = strchr(path+1,'/'))&&(slash2 = strchr(slash1+1,'/'))) return;
    hostName = path+1; field = slash2+1;
    *slash1 = *slash2 = '\0';  /* terminate parsed fields */

    /* Prepare to modify the ListView */
    AttachClientList(TrackWindow, FALSE);

    if (data)
    {
      UNLESS(modme = FindClientByName(hostName))
      {
        ULONG secsSince = time(NULL)-lastUpdateTime;

        /* If he's new, send him a message so he knows how long since we've been updated */
        *slash1 = '/';  /* re-concatenate the /hostName/QAmiTrack part */
        (void)QMessageOp(session, path, &secsSince, sizeof(ULONG));
        (void)QGo(session, 0L);
        *slash1 = '\0';

        modme = AddClient(hostName);
      }
      if (modme) UpdateClientField(modme, slash2+1, data);
    }
    else 
    {
      /* NULL data means he's logging out! */
      if (modme = FindClientByName(hostName))
      {
        RecordEvent(LOG_LOGOFF, modme, NULL, NULL, NULL);
        Remove((struct Node *)modme);
        clientListLength--;
        FreeClient(modme);
      }
    }
  }

  {
    int maxNameLen=0;
    BOOL BReformat = FALSE;
    struct Client * next;    

    AttachClientList(TrackWindow, FALSE);
    
    /* Recalculate the display strings */
    next = (struct Client *)clientList.lh_Head;
    while(next->node.ln_Succ)
    {
      int len = getDisplayIndexString(next)->bufLen;
      if (maxNameLen < len) maxNameLen = len;
      next = next->node.ln_Succ;
    }
    BReformat = (maxNameLen != currentNameLen);
    currentNameLen = maxNameLen;

    /* Now regenerate all listStrings */
    next = (struct Client *)clientList.lh_Head;
    while(next->node.ln_Succ)
    {
      struct String * oldString = next->listString;
  
      if ((BReformat)||(*(oldString->buffer)=='\0'))
      {
        struct String * newString;
      
        if (newString = MakeNewDisplayString(next, maxNameLen))
        { 
          if (oldString) FreeString(oldString);
          next->listString = newString;
          next->node.ln_Name = next->listString->buffer;
        }
      }
      next = (struct Client *)next->node.ln_Succ;
    }
    
    SortClientList();
  }
  AttachClientList(TrackWindow, TRUE);
}


void ScrollClient(struct Client * c, int scrollBy)
{
  struct String * newString;

  if (newString = NewString(""))
  {
    c->viewOffset += scrollBy;
    if (c->viewOffset < 0) c->viewOffset = 0;
    
    AttachClientList(TrackWindow, FALSE);  /* avoid dangling pointer */
    
    /* Put the client in a state where it will be updated! */
    FreeString(c->listString);
    c->listString = newString;

    UpdateClientList(NULL);  /* list will be reattached to window here */
  }    
}


void SetViewBy(ULONG ulItemCode)
{					        
  if (nViewBy != ulItemCode) 
  {
    nViewBy = ulItemCode;
    currentNameLen = -1;  /* Force redraw of whole list */
    UpdateClientList(NULL); 
  }
}

void SetSortBy(ULONG ulItemCode)
{					        
  if (nSortBy != ulItemCode) 
  {
    nSortBy = ulItemCode;
    currentNameLen = -1;  /* Force redraw of whole list */
    UpdateClientList(NULL); 
  }
}

BOOL SetLogViewLevel(ULONG ulItemCode)
{					        
  if (nLogViewLevel != ulItemCode) 
  {
    nLogViewLevel = ulItemCode;
    return(TRUE);
  }
  return(FALSE);
}

/* Returns any additional action flags that we want set */
void HandleIDCMP(struct WindowStuff * win)
{
	struct IntuiMessage *message;
	ULONG class, code, qual, ulItemCode, id;
	struct Gadget * gad;
	struct MenuItem * mItem;
	
	if (win == NULL) SetTrackFlag(CODE_QUIT);

	/* Examine pending messages */	
	while (message = (struct IntuiMessage *)GT_GetIMsg(win->win->UserPort))
	{
		class = message->Class;		/* extract needed info from message */
		code  = message->Code;
		qual  = message->Qualifier;
		gad   = (struct Gadget *) (message->IAddress);

		/* tell Intuition we got the message */
		GT_ReplyIMsg(message);

		/* see what events occured, take correct action */
		switch(class)
		{		
			case IDCMP_CLOSEWINDOW: 
				SetTrackFlag(CODE_HIDE);
				break;

            case IDCMP_NEWSIZE:
                {
                  char temp[50];
                  UpdateWindow(win,FALSE);
                  sprintf(temp, "Resize: X=%i, Y=%i, W=%i, H=%i",
                    win->win->LeftEdge,  win->win->TopEdge,
                    win->win->Width,     win->win->Height);
                  StatMessage(temp);
                }
                break;
      	
            case IDCMP_INTUITICKS:
                break;
      			
            case IDCMP_MOUSEBUTTONS:
                break;
      				
			case IDCMP_VANILLAKEY: 
			    { 
			      char buf[2];
			      
			      buf[0] = code; buf[1] = '\0';
			      SetCommentString(win, buf, FALSE);
			      ((struct StringInfo*)win->CommentString->SpecialInfo)->BufferPos = 1;  /* hack the cursor over to the right, dammit! */
			      ActivateGadget(win->CommentString, win->win, NULL);
			    }
				break;
		    
		    case IDCMP_RAWKEY:
		        if (cLastEntryClicked)
		        {
		          switch(code)
		          {
		            case 'L': ScrollClient(cLastEntryClicked, -5000); break;
		            case 'M': ScrollClient(cLastEntryClicked,  10); break;
		            case 'N': ScrollClient(cLastEntryClicked,  1); break;
		            case 'O': ScrollClient(cLastEntryClicked, -1); break;
		          }
		        }
		        break;
		        
			case IDCMP_GADGETUP:
                switch(gad ? gad->GadgetID : -1)
                {
                   case GAD_CLIENTLIST: ListClicked(code); break;
			       case GAD_COMMENTSTRING: SetCommentString(TrackWindow, ((struct StringInfo*)win->CommentString->SpecialInfo)->Buffer, TRUE); break;
			       case GAD_SERVERSTRING: SetServerString(TrackWindow, ((struct StringInfo*)win->ServerString->SpecialInfo)->Buffer); break;
			       case GAD_PORTSTRING: SetPortNumber(TrackWindow, ((struct StringInfo*)win->PortString->SpecialInfo)->LongInt); break;
			       case GAD_ACTIONCYCLE: SetCurrentActionIndex(TrackWindow,code); break;
			       default: printf("HandleICMP:  unknown gadget ID %i\n", id); break;
			    }
                break;

			case IDCMP_MENUPICK:
			    {
			      BOOL BUpdateWin = FALSE;
			      
				  while(code != MENUNULL) 
			      {
					char szMessage[150];
					mItem = ItemAddress( Menu, code );

					ulItemCode = (ULONG) GTMENUITEM_USERDATA(mItem);
					switch(ulItemCode)
					{
						case P_HIDE:
							SetTrackFlag(CODE_HIDE);
							break;
							
						case P_ABOUT:	    
							sprintf(szMessage,"%s\nby Jeremy Friesner\njfriesne@ucsd.edu\nCompiled: %s",
								pcDisplayVersionString,__DATE__);
							MakeReq(NULL,szMessage,"Not Bad");
							break;
							
						case P_QUIT:
						    SetTrackFlag(CODE_QUIT);
						    break;

					    case O_CONFIRM:
					        BConfirmAppLaunch = BConfirmAppLaunch ? FALSE : TRUE;
					        break;
					        
					    case O_RAGGED:
					        BRaggedText = !BRaggedText;
					        currentNameLen = -1;  /* Force re-creation of display list */
					        UpdateClientList(NULL);
					        break;

					    case O_BEEPONLOG:
					        BBeepOnLog = !BBeepOnLog;
					        break;
					        
					    case O_ENABLED:
					        SetTrackFlag(BEnabled ? CODE_DISABLE : CODE_ENABLE);
					        break;
					        
					    case O_LOGNONE:     nLogLevel = OUTPUT_NONE;     break;
					    case O_LOGTERSE:    nLogLevel = OUTPUT_TERSE;    break;
					    case O_LOGMODERATE: nLogLevel = OUTPUT_MODERATE; break;
					    case O_LOGVERBOSE:  nLogLevel = OUTPUT_VERBOSE;  break;
					    
					    case O_VIEWUSERNAME: case O_VIEWHOSTNAME: case O_VIEWREALNAME:
					        SetViewBy(ulItemCode);
					        break;
					        
					    case O_SORTBYAGE: case O_SORTBYNAME: case O_SORTBYCOMMENT: case O_SORTBYNONE:
					        SetSortBy(ulItemCode);
					        break;
					        
                        case O_LOGVIEWOFF: case O_LOGVIEW25: case O_LOGVIEW50: case O_LOGVIEW75: case O_LOGVIEWFULL:
                            BUpdateWin = (ulItemCode != nLogViewLevel);
                            SetLogViewLevel(ulItemCode);
                            break;
                        
                        case O_CLEARLOGVIEW:
                            ClearLogView();
                            break;

                        case O_REVERTCOMMENT:
                            SetCommentString(TrackWindow, origComment, TRUE);
                            break;
					}
				    code = mItem->NextSelect;
				  }
				  
				  if (BUpdateWin) UpdateWindow(win, FALSE);
				}
				break;
			
			case IDCMP_REFRESHWINDOW:
				GT_BeginRefresh(win->win);
				GT_EndRefresh(win->win, TRUE);
				nWinWidth  = win->win->Width;
				nWinHeight = win->win->Height;
				nWinLeft   = win->win->LeftEdge;
				nWinTop    = win->win->TopEdge;
				break;
				       
			default:        
				printf("handleIDCMP: bad class %lu\n",class);
				break;
		}
	}
}

struct CxStuff * SetupCxStuff(struct CxStuff * cx, int pri, char * hotkey)
{
    if (cx)
    {
       if (cx->name) FreeMem(cx->name, strlen(cx->name)+1);
       if (cx->broker) DeleteCxObjAll(cx->broker);
       if (cx->port)
       {
         /* deallocate cx */
         CxMsg * msg;
       
         while(msg = (CxMsg *)GetMsg(cx->port)) ReplyMsg((struct Message *)msg);
         DeletePort(cx->port);
       }
       FreeMem(cx, sizeof(struct CxStuff));  
       return(NULL);
    }
    else
    {
       char name[200];
       CxObj * filter;
       
       /* allocate and return a new cx */
       UNLESS(cx = AllocMem(sizeof(struct CxStuff), MEMF_CLEAR)) return(NULL);
       UNLESS(cx->port = CreateMsgPort()) return(SetupCxStuff(cx,0,NULL));

       if (BAllowMultiple) sprintf(name,"QAmiTrack %lu",time(NULL));
                      else strcpy(name,"QAmiTrack");
                      
       UNLESS(cx->name = AllocMem(strlen(name)+1, MEMF_ANY)) return(SetupCxStuff(cx,0,NULL));
       strcpy(cx->name, name);
       
       cx->nb.nb_Version = NB_VERSION;
       cx->nb.nb_Name    = cx->name;
       cx->nb.nb_Title   = pcDisplayVersionString;
       cx->nb.nb_Descr   = "Lists other Amigas on the 'net";
       cx->nb.nb_Unique  = NBU_UNIQUE | NBU_NOTIFY;   
       cx->nb.nb_Flags   = COF_SHOW_HIDE;  /* We'll have a window available */
       cx->nb.nb_Pri     = pri;
       cx->nb.nb_Port    = cx->port;
       cx->nb.nb_ReservedChannel = 0;
    
       UNLESS(cx->broker = CxBroker(&cx->nb, NULL)) return(SetupCxStuff(cx,0,NULL));
       if ((hotkey)&&(filter=CxFilter(hotkey))) 
       {
         CxObj *sender;
         
         AttachCxObj(cx->broker, filter);
         if (sender = CxSender(cx->port, EVT_HOTKEY)) 
         {
           CxObj *translate;
           
           AttachCxObj(filter,sender);
           if (translate = CxTranslate(NULL)) AttachCxObj(filter, translate);
         }
       }
       
       ActivateCxObj(cx->broker, 1L); /* Makes us "active" -- receiving messages */
       if (BEnabled == FALSE) ActivateCxObj(cx->broker, 0L);  /* Now disable it */
       return(cx);    
    }
}

struct WindowStuff * SetupTrackWindow(struct WindowStuff * win)
{
 if (win)
 {
   /* Free the window */
   UpdateWindow(win,TRUE);	/* Free all the gadgets */
   if (win->fontdata) 	    CloseFont(win->fontdata);
   if (win->fixedfontdata)	CloseFont(win->fixedfontdata);
   if (win->win)    	    
   {
     nWinTop  = win->win->TopEdge;
     nWinLeft = win->win->LeftEdge;
     CloseWindow(win->win);
   }
   if (win->screen) 	    UnlockPubScreen(NULL,win->screen);
		
   FreeMem(win,sizeof(struct WindowStuff));
   return(NULL);
 }
 else
 {
   UNLESS(win = AllocMem(sizeof(struct WindowStuff), MEMF_CLEAR)) return(NULL);
		
   /* Find the default public screen */
   UNLESS(win->screen = LockPubScreen(NULL)) return(SetupTrackWindow(win));

   AskFont(&win->screen->RastPort, &win->font);
   UNLESS(win->fontdata = OpenDiskFont(&win->font)) return(SetupTrackWindow(win));

   nMinWinHeight = win->screen->WBorTop + win->screen->RastPort.TxHeight + VSPACE + 3*(win->font.ta_YSize + VSPACE + VSPACE) + win->screen->WBorBottom + VSPACE;
   nMinWinWidth  = win->screen->WBorLeft + (HSPACE * 25) + win->screen->WBorRight;
		
   if (nWinWidth  < nMinWinWidth)  nWinWidth = nMinWinWidth;
   if (nWinHeight < nMinWinHeight) nWinHeight= nMinWinHeight;
		
   /* Open the window */
   UNLESS(win->win = OpenWindowTags(NULL,
    WA_Left,	    nWinLeft,
	WA_Top,	        nWinTop,
	WA_Width,	    nWinWidth,
	WA_Height,	    nWinHeight,
	WA_MinWidth,	nMinWinWidth,
	WA_MinHeight,	nMinWinHeight,
	WA_PubScreen,	win->screen,
	WA_PubScreenFallBack, TRUE,
	WA_MaxWidth,	-1,
	WA_MaxHeight,	-1,
	WA_Title,	    pcDisplayVersionString, 
	WA_CloseGadget, TRUE,
	WA_DepthGadget, TRUE,
	WA_SizeGadget,  TRUE,
	WA_Activate,	TRUE,
	WA_DragBar,	    TRUE,
	WA_SizeBBottom, TRUE,
	WA_Flags,       WFLG_NEWLOOKMENUS,
	WA_IDCMP,       IDCMP_REFRESHWINDOW | IDCMP_CLOSEWINDOW | 
		            IDCMP_MENUPICK | IDCMP_NEWSIZE | IDCMP_VANILLAKEY | 
		 		    IDCMP_RAWKEY | BUTTONIDCMP | LISTVIEWIDCMP |
		 		    CYCLEIDCMP | STRINGIDCMP,
    TAG_DONE)) return(SetupTrackWindow(win));

   AskFont(win->win->RPort, &win->fixedfont);
   UNLESS(win->fixedfontdata = OpenDiskFont(&win->fixedfont)) return(SetupTrackWindow(win));
   UNLESS(UpdateWindow(win,FALSE)) return(SetupTrackWindow(win));
   return(win);
   }
}

void debug(int n)
{
	printf("enter debug %i ... ",n); fflush(stdout);
	Delay(50);
	printf("continuing.\n"); fflush(stdout);
}

VOID wbmain(struct WBStartup *wbargv)
{    
	BStartedFromWB = TRUE;
	main(0,wbargv);
}

/* return TRUE iff the string represents the boolean value true */
BOOL ParseBool(char * string)
{
  char temp[100];
  
  strncpy(temp,string,sizeof(temp));
  temp[99]=0;
  
  ToLower(temp);
  
  return(
    (*temp=='\0')              ||  /* then it's just KEYWORD, so yes, right? */
    (strcmp("true",temp) == 0) ||
    (strcmp("yes",temp)  == 0) ||
    (strcmp("1",temp)    == 0));   /* Anyone who specifies 1 for true is a weenie! */
}


int ParseOutputLevel(char * keyword)
{
  ToLower(keyword);
  UNLESS(strcmp(keyword, "terse"))    return OUTPUT_TERSE;
  UNLESS(strcmp(keyword, "moderate")) return OUTPUT_MODERATE;
  UNLESS(strcmp(keyword, "verbose"))  return OUTPUT_VERBOSE;  
  return OUTPUT_NONE;  /* default */
}

int ParseLogViewLevel(char * keyword)
{
  ToLower(keyword);
  UNLESS(strcmp(keyword,  "off"))     return O_LOGVIEWOFF;
  UNLESS(strncmp(keyword, "0",  1))   return O_LOGVIEWOFF;
  UNLESS(strncmp(keyword, "25", 2))   return O_LOGVIEW25;
  UNLESS(strncmp(keyword, "50", 2))   return O_LOGVIEW50;
  UNLESS(strncmp(keyword, "75", 2))   return O_LOGVIEW75;
  UNLESS(strncmp(keyword, "100",3))   return O_LOGVIEWFULL;
  UNLESS(strcmp(keyword, "full"))     return O_LOGVIEWFULL;  
  return O_LOGVIEWOFF;  /* default */
}

int ParseViewBy(char * keyword)
{
  ToLower(keyword);

  UNLESS(strcmp(keyword, "username")) return O_VIEWUSERNAME;  
  UNLESS(strcmp(keyword, "realname")) return O_VIEWREALNAME;
  return O_VIEWHOSTNAME;  /* default */
}

int ParseSortBy(char * keyword)
{
  ToLower(keyword);

  UNLESS(strcmp(keyword, "age"))     return O_SORTBYAGE;  
  UNLESS(strcmp(keyword, "name"))    return O_SORTBYNAME;
  UNLESS(strcmp(keyword, "comment")) return O_SORTBYCOMMENT;
  UNLESS(strcmp(keyword, "none"))    return O_SORTBYNONE;
  
  return O_SORTBYAGE;  /* default */
}

char * GetViewByName(int k)
{
  switch(k)
  {
    case O_VIEWUSERNAME: return("username");
    case O_VIEWHOSTNAME: return("hostname");
    case O_VIEWREALNAME: return("realname");
    default: return("error");
  }
}

char * GetSortByName(int k)
{
  switch(k)
  {
    case O_SORTBYAGE:     return("age");
    case O_SORTBYNAME:    return("name");
    case O_SORTBYCOMMENT: return("comment");
    case O_SORTBYNONE:    return("nothing");
    default: return("error");
  }
}

char * GetOutputLevelName(int id)
{
  switch(id)
  {
    case OUTPUT_NONE:     return("none");
    case OUTPUT_TERSE:    return("terse");
    case OUTPUT_MODERATE: return("moderate");
    case OUTPUT_VERBOSE:  return("verbose");
    default:              return("error");
  }
}

void SetLogViewLength(int numLines)
{
  if (numLines >= 0)
  {
    AttachLogList(TrackWindow, FALSE);
    while(logListLen > numLines)
    {
      struct Node * top = RemHead(&logList);
      if (top) 
      {
        FreeMem(top,sizeof(struct Node)+strlen(top->ln_Name)+1);
        logListLen--;
      }
    }
    AttachLogList(TrackWindow, TRUE);
    maxLogListLen = numLines;
  }
}

void SetRexxScript(int which, char * name)
{
  if ((which >= 0)&&(which < NUM_LOG_ENUMS))
  {
    char * Old = rexxScripts[which];
    char * New = strdup(name);
    
    if (New)
    {
      rexxScripts[which] = New;
      if (Old) free(Old);
    }
  }
}

void FreeClientList(void)
{
  struct Client * next;
  
  while(next = (struct Client *) RemHead(&clientList)) FreeClient(next);
  clientListLength = 0;
}

/* Always called when AmiTrack exits, does any necessary cleanup */
void Cleanup(void)
{
    int i;
    
    RecordEvent(LOG_END, NULL, NULL, NULL, NULL);
    
    ClearLogView();
    
    for (i=0; i<NUM_LOG_ENUMS; i++) if (rexxScripts[i]) free(rexxScripts[i]);
    
    if (szLogFileName) free(szLogFileName);
    if (TS)          SetupTimer(TS);
    if (rexxHost)    CloseDownARexxHost(rexxHost);
    if (CX)          SetupCxStuff(CX,0,NULL);
	if (ttypes)      ArgArrayDone();    
	if (TrackWindow) SetupTrackWindow(TrackWindow);
    if (session)     QFreeSession(session);
    if (myInfo)      FreeClient(myInfo);
    
    FreeActions();
    FreeClientList();
	ReplaceAllocedString(&szServerName, NULL);

    if (AMarqueeBase)  CloseLibrary(AMarqueeBase);
	if (DiskFontBase)  CloseLibrary(DiskFontBase);
	if (GadToolsBase)  CloseLibrary(GadToolsBase);
	if (IntuitionBase) CloseLibrary(IntuitionBase);
	if (GraphicsBase)  CloseLibrary(GraphicsBase);
	if (WorkbenchBase) CloseLibrary(WorkbenchBase);
	if (IconBase)      CloseLibrary(IconBase);
	if (CxBase)        CloseLibrary(CxBase);	
}

void Disconnect(char * optStat)
{
  if (session)
  {
    QFreeSession(session);
    session = NULL;
    RecordEvent(LOG_DISCONNECT, NULL, NULL, optStat, NULL);
    BConnected = FALSE;
  }
  if (optStat) StatMessage(optStat);
 
  /* Clear the client list! */
  AttachClientList(TrackWindow, FALSE);
  FreeClientList();
  AttachClientList(TrackWindow, TRUE);
}

void Reconnect(void)
{
  char szMessage[400];            
  char szInterest[] = "/#?/QAmiTrack/(comment|username|realname|winopen)";
  int maxFieldLen = 1024;
  
  UNLESS(BEnabled) return;
  
  Disconnect(NULL);
  UNLESS(*szServerName)
  {
    StatMessage("Can't connect, no server name given.");
    return;
  }
  
  if (session = QNewSessionAsync(szServerName, nPort, "QAmiTrack"))
  { 
    sprintf(szMessage,"Connecting to [%s:%i]",szServerName,nPort);
    StatMessage(szMessage);
    
    /* these won't get processed until the connection is ready! */
    sprintf(szMessage,"/%s/QAmiTrack", szAccessTo);
    UNLESS((QSetMessageAccessOp(session, "/#?/QAmiTrack", sizeof(ULONG))) &&
           (QSetAccessOp(session, szMessage)) &&
           (QRequestPrivilegesOp(session, QPRIV_GETSYSMESSAGES)) &&
           (QGetOp(session, szInterest, maxFieldLen)) &&
           (QSubscribeOp(session, szInterest, maxFieldLen)) &&
           (SendState(STATE_ALL)) &&
           (QGo(session, 0L)))
    {
      sprintf(szMessage, "Couldn't log in to server [%s:%i]",szServerName,nPort);
      Disconnect(szMessage);
    }
  }
  else
  {				
    if (szServerName)
    {
      sprintf(szMessage,"Couldn't connect to %s:%i",szServerName, nPort);
      StatMessage(szMessage);
    }
  }
}


void StatMessage(char * message)
{
	static char szMessage[100] = "AmiTrack ready.";
	
	if (message) strncpy(szMessage,message,sizeof(szMessage));
	szMessage[99] = '\0';
		
	if ((TrackWindow)&&(TrackWindow->win)) 
	  SetWindowTitles(TrackWindow->win, szMessage, (char *)~0);
}

#define UPDATEMENUFLAG(item, variable, flag) {if (variable) item->Flags |= flag; else item->Flags &= ~(flag);}
void SetMenuValues(void)
{
 struct Menu *currentMenu = Menu;  /* Project Menu */
 struct MenuItem *currentItem, *currentSub;
 
 UNLESS(TrackWindow) return;
 ClearMenuStrip(TrackWindow->win);

 currentMenu = currentMenu->NextMenu;  /* Options menu */
 
 currentItem = currentMenu->FirstItem;	/* Enabled */
 UPDATEMENUFLAG(currentItem, BEnabled, CHECKED);
          
 currentItem = currentItem->NextItem;   /* Confirm */
 UPDATEMENUFLAG(currentItem, BConfirmAppLaunch, CHECKED);
 UPDATEMENUFLAG(currentItem, Actions[0],        ITEMENABLED);
                    	 
 currentItem = currentItem->NextItem;   /* Ragged Text */
 UPDATEMENUFLAG(currentItem, BRaggedText, CHECKED);

 currentItem = currentItem->NextItem;   /* Beep On Log */
 UPDATEMENUFLAG(currentItem, BBeepOnLog, CHECKED);

 currentItem = currentItem->NextItem;   /* Log Events */

   currentSub = currentItem->SubItem;  /* Log None  */
   UPDATEMENUFLAG(currentSub, nLogLevel==OUTPUT_NONE, CHECKED);

   currentSub = currentSub->NextItem;  /* Log Terse */
   UPDATEMENUFLAG(currentSub, nLogLevel==OUTPUT_TERSE, CHECKED);

   currentSub = currentSub->NextItem;  /* Log Moderate */
   UPDATEMENUFLAG(currentSub, nLogLevel==OUTPUT_MODERATE, CHECKED);

   currentSub = currentSub->NextItem;  /* Log Verbose */
   UPDATEMENUFLAG(currentSub, nLogLevel==OUTPUT_VERBOSE, CHECKED);
 
 currentItem = currentItem->NextItem;  /* View By... */

   currentSub  = currentItem->SubItem;   /* Host Name */
   UPDATEMENUFLAG(currentSub, (nViewBy==O_VIEWHOSTNAME), CHECKED);

   currentSub  = currentSub->NextItem;   /* User Name */
   UPDATEMENUFLAG(currentSub, (nViewBy==O_VIEWUSERNAME), CHECKED);

   currentSub  = currentSub->NextItem;   /* Real Name */
   UPDATEMENUFLAG(currentSub, (nViewBy==O_VIEWREALNAME), CHECKED);

 currentItem = currentItem->NextItem;   /* Sort By... */
 
   currentSub  = currentItem->SubItem;   /* Age */
   UPDATEMENUFLAG(currentSub, (nSortBy==O_SORTBYAGE), CHECKED);
   
   currentSub  = currentSub->NextItem;   /* Name */
   UPDATEMENUFLAG(currentSub, (nSortBy==O_SORTBYNAME), CHECKED);
   
   currentSub  = currentSub->NextItem;   /* Comment */
   UPDATEMENUFLAG(currentSub, (nSortBy==O_SORTBYCOMMENT), CHECKED);
   
   currentSub  = currentSub->NextItem;   /* None */
   UPDATEMENUFLAG(currentSub, (nSortBy==O_SORTBYNONE), CHECKED);

 currentItem = currentItem->NextItem;   /* Log View... */
 
   currentSub  = currentItem->SubItem;   /* Off */
   UPDATEMENUFLAG(currentSub, (nLogViewLevel==O_LOGVIEWOFF), CHECKED);
   
   currentSub  = currentSub->NextItem;   /* 25% */
   UPDATEMENUFLAG(currentSub, (nLogViewLevel==O_LOGVIEW25), CHECKED);
   
   currentSub  = currentSub->NextItem;   /* 50% */
   UPDATEMENUFLAG(currentSub, (nLogViewLevel==O_LOGVIEW50), CHECKED);
   
   currentSub  = currentSub->NextItem;   /* 75% */
   UPDATEMENUFLAG(currentSub, (nLogViewLevel==O_LOGVIEW75), CHECKED);

   currentSub  = currentSub->NextItem;   /* Full */
   UPDATEMENUFLAG(currentSub, (nLogViewLevel==O_LOGVIEWFULL), CHECKED);
   
 ResetMenuStrip(TrackWindow->win, Menu);                 
 return;	
}

/* accepts a command string of the following format:

    "NAME,COMMAND"
    
   To insert, update, or delete an action.  e.g.
   
    "AmiPhone,sys:utilities/AmiPhone CONNECT=%s"
    
   Would add a command named AmiPhone to the cycle gadget.
   
   To delete an action, specify the name without a command.  e.g.
   
    "AmiPhone"
    
   Returns the index of the new (or removed) command, or -1 if 
   there was an error (out of memory or all 20 spots used)
   
*/ 
int UpdateActions(struct WindowStuff * win, char * string)
{
  int prevGadWidth;
  char name[75] = "", command[300] = "", *pcTemp;
  int index = -1, i;
  
  if (win) prevGadWidth = getActionCycleWidth(win);
  
  if (pcTemp = strchr(string, ',')) *pcTemp = '\0';
  strncpy(name, string, sizeof(name));
  if (pcTemp)
  {
    *pcTemp = ','; pcTemp++;
    strncpy(command, pcTemp, sizeof(command));
  }
  
  /* Check current index to see if the item is already present */
  for (i=0; ((ActionLabels[i])&&(i<20)); i++)
  {
    if (strcmp(name, ActionLabels[i]) == 0) 
    {
      index = i; 
      break;
    }
  }
  
  if (index >= 0)
  {
    /* Replace/Delete existing command */
    if (command[0]) ReplaceAllocedString(&Actions[index], command);
    else 
    {
      /* delete this entry and move the following entries back one! */
      ReplaceAllocedString(&ActionLabels[index], NULL);
      ReplaceAllocedString(&Actions[index], NULL);
      for (int i=index; i<19; i++)
      {
        ActionLabels[i] = ActionLabels[i+1];
        Actions[i]      = Actions[i+1];
      }
      Actions[19] = ActionLabels[19] = NULL;
      nNumActions--;
          
      /* Shrink cycle gadget if necessary */
      if ((win)&&(getActionCycleWidth(win) < prevGadWidth)) UpdateWindow(win, FALSE);
      SetCurrentActionIndex(win, currentAction);
      return(index);
    }
  }
  else
  {
    /* Add new command */
    if ((name[0])&&(command[0]))
    {
      for (i=0; i<20; i++)
      {
        if (ActionLabels[i] == NULL)
        {
          ReplaceAllocedString(&ActionLabels[i], name);
          ReplaceAllocedString(&Actions[i], command);
          nNumActions++;
          index = i;
          if (currentAction < 0) currentAction = 0;
          break;
        }
      }
    }
  }
  
  /* grow cycle gadget if necessary */
  if ((win)&&(getActionCycleWidth(win) > prevGadWidth)) UpdateWindow(win, FALSE);
  SetCurrentActionIndex(win, currentAction);
  return(index);
}

BOOL isDeviceAvailable(char * name)
{
  struct DosList * dlist;
  BOOL ret = FALSE;
  ULONG flags = LDF_ALL | LDF_WRITE;
  
  if (dlist = LockDosList(flags))
  {
    ret = (FindDosEntry(dlist, name, flags) != NULL);
    UnLockDosList(flags);
  }
  return(ret);
}


int main(int argc, char **argv)
{
  BOOL BOpenWin;
  char * hotkey, * action, * userName, * realName;
  long pri;
  char * fakeArgv[2];
  int i=0;
    
  NewList(&clientList);
  NewList(&logList);
  memset(rexxScripts, 0, sizeof(rexxScripts));

  /* Disable DICE's stupid CTRL-C handling--we really can't have ungraceful exits */
  signal(SIGINT, SIG_IGN);
    
  /* fix bug when called from CLI with no args */
  if ((BStartedFromWB == FALSE)&&(argc==1))
  {
    /* fake an arg */
    argc = 2;
    argv = fakeArgv;
      
    fakeArgv[0] = argv[0];
    fakeArgv[1] = "\0";
  }
  if ((BStartedFromWB == FALSE)&&(argc>=2)&&(*argv[1]=='?'))
  {
    printf("Usage: QAmiTrack SERVER/K,PORT/K/N,COMMENT/K,CX_POPUP/K,\n"
           "                 CX_PRIORITY/K/N,CX_POPKEY/K,ENABLE/K,\n"
           "                 ALLOWMULTIPLE/K,CONFIRMAPPLAUNCH/K,\n"
           "                 ACTION1/K,ACTION2/K,...,LEFT/K/N,TOP/K/N,\n"
           "                 WIDTH/K/N,HEIGHT/K/N,LOGFILE/K,LOGLEVEL/K,\n"
           "                 RECONNECTDELAY/K/N,RAGGEDTEXT/K,VIEWBY/K\n"
           "                 SORTBY/K,USERNAME/K,REALNAME/K,VISIBLETO/K\n"
           "                 LOGONSCRIPT/K,LOGOFFSCRIPT/K,UPDATESCRIPT/K\n"
           "                 CONNECTSCRIPT/K,ENDSCRIPT/K,DISCONNECTSCRIPT/K\n"
           "                 DISCONNECTSCRIPT/K,STARTSCRIPT/K,BEEPONLOG/K\n"
           "                 LOGVIEWLEVEL/K,LOGVIEWLENGTH/K/N\n");
    exit(5);
  }
   
  InitActions();

  atexit(Cleanup);
  
  UNLESS(IconBase = OpenLibrary("icon.library",33))
	TrackExit("Couldn't open icon.library 33+",RETURN_ERROR);

  UNLESS(CxBase = OpenLibrary("commodities.library", 37L))
    TrackExit("Couldn't open commodities.library v37+", RETURN_ERROR);
	    				
  UNLESS(WorkbenchBase = OpenLibrary("workbench.library",37))
  	TrackExit("Couldn't open icon.library 37+",RETURN_ERROR);

  UNLESS(GraphicsBase = OpenLibrary("graphics.library", 37))
    TrackExit("Couldn't open graphics.library 37+",RETURN_ERROR);

  UNLESS(IntuitionBase = OpenLibrary("intuition.library", 37))
    TrackExit("Couldn't open intuition.library 37+",RETURN_ERROR);

  UNLESS(AMarqueeBase = OpenLibrary("amarquee.library", 46L))
    TrackExit("Couldn't open amarquee.library v46+", RETURN_ERROR);
	    
  UNLESS(GadToolsBase = OpenLibrary("gadtools.library", 37))
    TrackExit("Couldn't open gadtools.library 37+",RETURN_ERROR);

  UNLESS(DiskFontBase = OpenLibrary("diskfont.library", 37))
    TrackExit("Couldn't open diskfont.library 37+",RETURN_ERROR);
    
  /* Parse startup arguments */
  UNLESS(ttypes = ArgArrayInit(argc, argv))
      TrackExit("Couldn't read startup arguments", RETURN_ERROR);

  nWinLeft    = ArgInt(   ttypes, "LEFT",         nWinLeft);
  nWinTop	  = ArgInt(   ttypes, "TOP",          nWinTop);
  nWinWidth   = ArgInt(   ttypes, "WIDTH",        nWinWidth);
  nWinHeight  = ArgInt(   ttypes, "HEIGHT",       nWinHeight);
  nPort       = ArgInt(   ttypes, "PORT",         DEFAULT_AMITRACK_PORT);
  origComment = PastSpaces(ArgString(ttypes, "COMMENT",      DEFAULT_AMITRACK_COMMENT));
  pri         = ArgInt(   ttypes, "CX_PRIORITY",  DEFAULT_AMITRACK_CXPRI);
  nReconnectDelay = ArgInt(ttypes, "RECONNECTDELAY", DEFAULT_AMITRACK_RECONNECTDELAY);
  hotkey      = ArgString(ttypes, "CX_POPKEY",    DEFAULT_AMITRACK_POPKEY);
  szAccessTo  = ArgString(ttypes, "VISIBLETO",    DEFAULT_AMITRACK_ACCESSTO);
  szLogFileName = strdup(ArgString(ttypes, "LOGFILE",      DEFAULT_AMITRACK_LOGFILE));  /* strdup() it because we may need to change it! */
  nViewBy     = ParseViewBy(ArgString(ttypes, "VIEWBY",      DEFAULT_AMITRACK_VIEWBY));
  nSortBy     = ParseSortBy(ArgString(ttypes, "SORTBY",      DEFAULT_AMITRACK_SORTBY));
  nLogLevel   = ParseOutputLevel(ArgString(ttypes, "LOGLEVEL",        DEFAULT_AMITRACK_LOGLEVEL));
  BOpenWin    = ParseBool(ArgString(ttypes, "CX_POPUP",     DEFAULT_AMITRACK_CXPOPUP));
  BAllowMultiple    = ParseBool(ArgString(ttypes, "ALLOWMULTIPLE",    DEFAULT_AMITRACK_ALLOWMULTIPLE));
  BEnabled          = ParseBool(ArgString(ttypes, "ENABLED",          DEFAULT_AMITRACK_ENABLED));
  BConfirmAppLaunch = ParseBool(ArgString(ttypes, "CONFIRMAPPLAUNCH", DEFAULT_AMITRACK_CONFIRMAPP));
  BRaggedText       = ParseBool(ArgString(ttypes, "RAGGEDTEXT",       DEFAULT_AMITRACK_RAGGEDTEXT));    
  BBeepOnLog        = ParseBool(ArgString(ttypes, "BEEPONLOG",        DEFAULT_AMITRACK_BEEPONLOG));    
  ReplaceAllocedString(&szServerName, ArgString(ttypes, "SERVER",       DEFAULT_AMITRACK_SERVER));
  nLogViewLevel    = ParseLogViewLevel(ArgString(ttypes, "LOGVIEWLEVEL", DEFAULT_AMITRACK_LOGVIEWLEVEL));
  maxLogListLen    = ArgInt(ttypes, "LOGVIEWLENGTH", DEFAULT_AMITRACK_LOGVIEWLENGTH);

  realName = ArgString(ttypes, "REALNAME", NULL);    
  userName = ArgString(ttypes, "USERNAME", NULL);    
  
  /* If not given, look in ENV: */
  UNLESS(userName) userName = getenv("USERNAME");
  UNLESS(userName) userName = getenv("USER");
  UNLESS(userName) userName = getenv("LOGNAME");
  UNLESS(userName) userName = "nobody";
  UNLESS(realName) realName = getenv("REALNAME");
  UNLESS(realName) realName = "anonymous";

  /* Record any ARexx scripts the user wants launched */
  for (i=0; i<NUM_LOG_ENUMS; i++) 
  {
    UNLESS(rexxScripts[i] = strdup(ArgString(ttypes, rexxScriptNames[i], "")))
      TrackExit("Couldn't allocate Rexx script name", RETURN_ERROR);
  }
  
  for (i=0; i<20; i++)
  {
    char temp[15];
    sprintf(temp,"ACTION%i",i);
      
    if (action = (ArgString(ttypes, temp, NULL)))
    {
      char * pcTemp = strchr(action,',');
      if (pcTemp)
      {
        *pcTemp = '\0';  /* temporarily separate the string */
        ReplaceAllocedString(&ActionLabels[nNumActions],action);
        ReplaceAllocedString(&Actions[nNumActions],pcTemp+1);
        *pcTemp = ',';
        nNumActions++;        
        currentAction = 0;
      }
    }
  }
    
  /* set globals */
  pcDisplayVersionString = &szVersionString[6];

  lLastChangeAt = time(NULL);

  /* Allocate our own copies of the given strings */
  UNLESS((myInfo = NewClient(""))&&
         (SetString(&myInfo->userName, userName))&&
         (SetString(&myInfo->realName, realName))&&
         (SetString(&myInfo->comment,  origComment)) &&
         (SetString(&myInfo->winOpen,  BOpenWin ? "Y" : "N")))
           TrackExit("Couldn't allocate local info", RETURN_ERROR);

  /* Force userName to be <= 8 chars */
  if (myInfo->userName->bufLen >= 9) myInfo->userName->buffer[8] = '\0';
	
  UNLESS(CX = SetupCxStuff(NULL, pri, hotkey))
    TrackExit("Couldn't setup Commodities Broker.  (AmiTrack already running?)",RETURN_OK);

  UNLESS(TS = SetupTimer(NULL)) TrackExit("Couldn't setup timer.",RETURN_ERROR);
        
  rexxHost = SetupARexxHost(CX->name, NULL);
    
  UNLESS((BOpenWin==FALSE)||(TrackWindow = SetupTrackWindow(NULL))) TrackExit("Couldn't open GUI", RETURN_ERROR);

  SetMenuValues();
	
  SetTrackFlag(CODE_RECONNECT);
  SetTimer(TS, nUpdateDelay, 0);

  RecordEvent(LOG_START, NULL, NULL, NULL, NULL);
  
  while(1)
  {
    if (TrackFlagsSet() == FALSE) TrackWait(TrackWindow, session, rexxHost, CX, TS);

    if (CheckTrackFlag(CODE_TIMER))
    {
      if (TrackWindow) UpdateWindowTimes();
      connectDelayLeft -= nUpdateDelay;
      if ((session == NULL)&&(connectDelayLeft < 0)) 
      {
        if (nReconnectDelay >= 0) SetTrackFlag(CODE_RECONNECT);
        connectDelayLeft = nReconnectDelay*60;
      }
      SetTimer(TS, nUpdateDelay, 0);
    }
    if (CheckTrackFlag(CODE_QUIT)) break;
    if (CheckTrackFlag(CODE_WINDOW_EVENT)) HandleIDCMP(TrackWindow);
    if (CheckTrackFlag(CODE_AREXX))        ARexxDispatch(rexxHost);
    if (CheckTrackFlag(CODE_ENABLE))
    {
      BEnabled = TRUE;
      ActivateCxObj(CX->broker, 1L);
      StatMessage("AmiTrack client is now enabled.");
      if (session == NULL) SetTrackFlag(CODE_RECONNECT);
    }
    if (CheckTrackFlag(CODE_DISABLE))
    {
      ActivateCxObj(CX->broker, 0L);
      BEnabled = FALSE;
      Disconnect("AmiTrack client is now disabled.");
    }
    if (CheckTrackFlag(CODE_RECONNECT)) Reconnect();
    if (CheckTrackFlag(CODE_QMESSAGE))
    {
      struct QMessage * msg;

      while(msg = GetMsg(session->qMsgPort))
      {
        if ((msg->qm_Status == QERROR_SYS_MESSAGE)&&(msg->qm_Path)&&(msg->qm_Data))
        {
           char * hostNameEnd = strchr(msg->qm_Path+1, '/');
           
           if (hostNameEnd)
           {
             *hostNameEnd = '\0';
             RecordEvent(LOG_SYSMESSAGE, NULL, NULL, msg->qm_Path+1, msg->qm_Data);          
             *hostNameEnd = '/';
           }
        }
        else if (msg->qm_Status == QERROR_UNPRIVILEGED)
        {
          /* Do nothing, it probably only means nobody heard our QMessageOp() */
        }
        else if (msg->qm_Status == QERROR_NO_ERROR)
        {
          if (msg->qm_ID > 0) UpdateClientList(msg);
          else if ((msg->qm_ID == 0)&&(msg->qm_Path)&&(msg->qm_Data))
          {
            if (BConnected == FALSE)
            {
              lastUpdateTime = time(NULL);
              StatMessage("Connected to server.");
              RecordEvent(LOG_CONNECT, NULL, NULL, NULL, NULL);
              BConnected = TRUE;  /* Note that our connection is "live" now! */
            }
            else
            {
              /* If ID is zero, then we know QMessage is a result
                 of someone doing a QMessageOp() at us! */
              UpdateClientTime(msg->qm_Path, *((ULONG *) msg->qm_Data));
              UpdateWindowTimes();
            }
          }
        }
        else /* error! */
        {
          char szMessage[250];
 
          sprintf(szMessage,"Disconnected: [%s] (line %i).", QErrorName(msg->qm_Status), msg->qm_ErrorLine);
          FreeQMessage(session, msg);  /* gotta free it now as were gonna break outta the loop! */
          Disconnect(BConnected ? szMessage : NULL);
          break;  /* Important to stop processing QMessages after the session has gone! */
        }
 
        /* free up message now that we're done with it! */ 
        FreeQMessage(session, msg);
      }
    }
    if ((CheckTrackFlag(CODE_HIDE))&&(TrackWindow)) 
    {
      TrackWindow = SetupTrackWindow(TrackWindow);
      SetString(&myInfo->winOpen,"N");
      SendState(STATE_WINOPEN);
    }
    if (CheckTrackFlag(CODE_SHOW))
    {
      if (TrackWindow) ActivateWindow(TrackWindow->win);
      else
      {
        TrackWindow = SetupTrackWindow(NULL);        
        StatMessage(NULL);  /* Show last message */ 
        SetString(&myInfo->winOpen,"Y");
        SendState(STATE_WINOPEN);
      }
    } 
    SetMenuValues();
  }
  
  /* Cleanup() called here! */
}
