/* TrafficSignal.c  (c) 1993 by Frank J. Perricone, All rights reserved.
   MS-DOS adaptations by Daniel C. Mecklenburg

This is used to create a report summarizing traffic levels in an echo.

Usage: TrafficSignal <path> [NAME] [POSTS] [SIZE] [DATE] [AFTER "date"] [LAST days]
                     [BEGIN msg] [END msg] [FORMAT "string"] [TO filename]

The <path> shows the complete pathname of the message area; it should end in
a slash or colon.  NAME, POSTS, SIZE, and DATE set the sort order (which
defaults to NAME).  You can't use both AFTER and LAST.  AFTER should be
followed by a date in Fido format, either Fido_11w ("01 Jan 86  02:34:56")
or Seadog ("Tue  1 Jan 86 01:23") and should be quote-enclosed.  LAST tells
to only use the last n days.  BEGIN and END set a range of messages; this
is typically used to omit 1.msg (a hiwater mark) by using BEGIN 2, but you
can use it for whatever you want.  (Note: you can have both a BEGIN &| END
criteria and an AFTER | LAST one, both will be applied.)  TO determines
the filename to send the result to, otherwise it goes to stdout.  LFORMAT
if present causes TrafficSignal to not make a chart but instead create a
file made up of the string you give, using printf() substitution.

The program scans all the posts in the echo that are numbered between the BEGIN
and END (if present) and posted after the AFTER or LAST dates (if present), and
the report summarizes them.  The list that is generated shows each person who's
posted, their Fido origin address, how many posts, when the most recent one was,
and the average post size.  These are output in a table format and the table is
sorted as described above.

  .key areapath,sortcrit,since
  .bra {
  .ket }
  TrafficSignal {areapath} {sortcrit} LAST {since} TO T:{$$}
  FidoEnter {areapath} T:{$$} {$$} "Traffic Signal" "All" "TrafficSignal Traffic Report" ORIG 1:325/611 DEST 1:325/611
  delete t:{$$} >NIL:

If you have a DOS program that'll build a Fido message from a text file, try
something like this.  I'm imagining a FIDOENTR.EXE program like my FidoEnter:

  @ECHO OFF
  TRAFSGNL %1 %2 LAST %3 TO C:\TRAFSGNL.RPT
  FIDOENTR %1 C:\TRAFSGNL.RPT "Traffic Signal" "All" "TrafficSignal Traffic Report"
  DEL C:\TRAFSGNL.RPT

*/

#include <stdio.h>
#include <stdlib.h>
#include "errorchk.h"

#ifdef AMIGA
#include <sys/dir.h>
#include <sys/stat.h>
#include <time.h>
#endif

#ifdef MS_DOS
#include <ctype.h>
#include <dirent.h>
#include <string.h>
#include <dir.h>
#include <time.h>
#include <sys\stat.h>
#endif

#define VERSION "1.3"

#define TRUE  1
#define FALSE 0

struct t_fido {
  unsigned int zone, net, node, point;
  };

struct t_date {
  int year, month, day, hour, minute, second;
  };

struct t_member {
  char   name[36];
  struct t_fido address;
  long   numposts, totpostsize;
  struct t_date lastpostdate;
  };

#define MAXMEMBERS 200
#define ORIGINLINE " * Origin: "
#define ORIGINLEN  11
#define DAYSIZE    60*60*24       /* seconds in a day */


short readmsg(char *msgpath, char *from, struct t_date *datetime, struct t_fido *origin, int *size);
short readkluge(char *buf, FILE *fp);
short instr(char *s, char find, short start);
short rev_instr(char *s, char find, short start);
int find_char(char tofind, char *s);
void parse_fido(char *s, struct t_fido *fido);
short fgetcr(char *buf, int maxlen, FILE *fp);

int find_member(struct t_member *members, int *nummembers, char *name);

int mnumber(char *s);
void   cvtdate(struct t_date *date,char *s);
char  *prtdate(struct t_date *date);
short  cmpdate(struct t_date *d1, struct t_date *d2);
void   cpydate(struct t_date *dest, struct t_date *src);
void   tm2date(struct t_date *dest, time_t *t);

#ifdef AMIGA
int sortcmp_name(struct t_member *arg1, struct t_member *arg2);
int sortcmp_post(struct t_member *arg1, struct t_member *arg2);
int sortcmp_date(struct t_member *arg1, struct t_member *arg2);
int sortcmp_size(struct t_member *arg1, struct t_member *arg2);
#endif

#ifdef MS_DOS
int sortcmp_name(const void *arg1, const void *arg2);
int sortcmp_post(const void *arg1, const void *arg2);
int sortcmp_date(const void *arg1, const void *arg2);
int sortcmp_size(const void *arg1, const void *arg2);
#endif

#define SORT_NAME   1
#define SORT_POSTS  2
#define SORT_SIZE   3
#define SORT_DATE   4


int main(int argc, char *argv[]) {
  struct t_fido orig;
  int i=0,j,nummembers=0;
  long allposts=0,allpostsize=0;
  char s[256],*ext,from[36];
  DIR *dptr;
  FILE *fp;
  struct t_member *members;
  struct t_date when,since;
  time_t t=time(NULL),sincet=t;
  struct tm *tp;
  short sorttype=SORT_NAME, begin=1, end=0;
  char lformat[256], tofname[256];


  #ifdef AMIGA
  struct direct *dentry;
  #endif

  #ifdef MS_DOS
  struct dirent *dentry;
  #endif

  fprintf(stderr,"\nTrafficSignal  %s  (c) 1993 by Frank J. Perricone\n\n",VERSION);

  checkusage(2,"TrafficSignal <path> [NAME] [POSTS] [SIZE] [DATE] [AFTER \"<date>\"] [LAST <days>]\n"
               "              [BEGIN <msg>] [END <msg>] [FORMAT \"<string>\"] [TO <filename>]\n\n",argc,argv);


  since.year = since.month = since.day = since.hour = since.minute = since.second = 0;
  tofname[0]='\0';
  lformat[0]='\0';

  for (i=2;i<argc;i++) {
    switch(toupper(argv[i][0])) {
      case 'N': sorttype=SORT_NAME;
                break;
      case 'P': sorttype=SORT_POSTS;
                break;
      case 'S': sorttype=SORT_SIZE;
                break;
      case 'D': sorttype=SORT_DATE;
                break;
      case 'A': cvtdate(&since,argv[++i]);
                break;
      case 'L': sincet -= atoi(argv[++i])*DAYSIZE;
                tm2date(&since,&sincet);
                break;
      case 'B': j=atoi(argv[++i]);
                if (j<1) {
                    fprintf(stderr,"WARNING: Minimum allowable value for BEGIN is 1; BEGIN command ignored\n");
                  } else {
                    begin=j;
                  }
                break;
      case 'E': j=atoi(argv[++i]);
                if (j<1) {
                    fprintf(stderr,"WARNING: Minimum allowable value for END is 1; END command ignored\n");
                  } else {
                    end=j;
                  }
                break;
      case 'T': strcpy(tofname,argv[++i]);
                break;
      case 'F': strcpy(lformat,argv[++i]);
                break;
      default:  fprintf(stderr,"\nIgnoring invalid parameter %s\n",argv[i]);
                break;
      }
    }

  if (begin>end) {
      fprintf(stderr,"WARNING: BEGIN value higher than END value!  Setting END to %i\n",begin);
      end=begin;
    }


  if ((members=malloc(sizeof(struct t_member)*MAXMEMBERS))==NULL) {
      fprintf(stderr,"\nError: could not allocate memory for member list.\n\n");
      exit(20);
    }

  #ifdef AMIGA
  makesure(dptr=opendir(argv[1]),"Fatal error: could not read the Fido directory!\n",20);
  #endif
  #ifdef MS_DOS
  makesure(dptr=opendir(argv[1])!=NULL,"Fatal error: could not read the Fido directory!\n",20);
  #endif

  while (dentry=readdir(dptr)) {
    ext=&(dentry->d_name[(strlen(dentry->d_name)-4)]);
    if (!stricmp(ext,".MSG") && atoi(dentry->d_name)>=begin && (end==0 || atoi(dentry->d_name)<=end)) {

        /* find out the info on the message */
        sprintf(s,"%s%s",argv[1],dentry->d_name);
        if (!readmsg(s,from,&when,&orig,&i)) continue; /* that is, loop again */
        fprintf(stderr,"\x1b[1M  #%4i from %s (%i:%i/%i.%i) on %s, size %i\r",atoi(dentry->d_name),from,orig.zone,orig.net,orig.node,orig.point,prtdate(&when),i);
        fflush(stderr);

        /* only log if not from TrafficSignal, and if since the "since" date */
        if (stricmp(from,"TrafficSignal") && stricmp(from,"Traffic Signal") && cmpdate(&when,&since)!=-1) {
            /* find this member in the list (add if needed) and update member info */
            j=find_member(members,&nummembers,from);
            members[j].numposts++;
            members[j].totpostsize += i;
            members[j].address.zone  = orig.zone;
            members[j].address.net   = orig.net;
            members[j].address.node  = orig.node;
            members[j].address.point = orig.point;
            if (cmpdate(&when,&(members[j].lastpostdate))==1) {
                cpydate(&(members[j].lastpostdate),&when);
              }
          }
      }
    }
  closedir(dptr);
  fprintf(stderr,"\x1b[1M  Sorting...\r");
  fflush(stderr);

  /* sort */
  switch(sorttype) {
    case SORT_NAME : qsort(members,nummembers,sizeof(struct t_member),sortcmp_name); break;
    case SORT_POSTS: qsort(members,nummembers,sizeof(struct t_member),sortcmp_post); break;
    case SORT_SIZE : qsort(members,nummembers,sizeof(struct t_member),sortcmp_size); break;
    case SORT_DATE : qsort(members,nummembers,sizeof(struct t_member),sortcmp_date); break;
    }
  fprintf(stderr,"\x1b[1M\r");
  fflush(stderr);

  /* generate report */

  if (tofname[0]!='\0') {
      fp=fopen_or_die(tofname,"w","\n\nError: could not open output file!\n\n",20);
    } else {
      fp=stdout;
    }

  if (lformat[0]=='\0') { /* print header info */
      fprintf(fp,"TrafficSignal %s  (c) 1993 by Frank J. Perricone\n\n",VERSION);
      fprintf(fp,"Report of echo traffic");
      if (since.year!=0) fprintf(fp," since %s",prtdate(&since));
      tp=localtime(&t);
      strftime(s,20,"%d %b %y %H:%M:%S",tp);
      fprintf(fp,", generated %s\n\n",s);
      fprintf(fp,"                                    FidoNet      Num   Avg\n");
      fprintf(fp,"     Name of Participant            Address     Posts Size  Date of Last Post\n");
      fprintf(fp,"=============================== =============== ===== ===== ==================\n");
    }

  for (i=0;i<nummembers;i++) {
    sprintf(s,"%i:%i/%i.%i",members[i].address.zone,members[i].address.net,members[i].address.node,members[i].address.point);
    if (lformat[0]=='\0') {
        members[i].name[31]='\0';  /* ensure it doesn't overflow its column */
        fprintf(fp,"%-31s ",members[i].name);
        s[15]='\0';
        fprintf(fp,"%-15s %5i %5i %s\n",s,members[i].numposts,members[i].totpostsize/members[i].numposts,prtdate(&(members[i].lastpostdate)));
        allposts += members[i].numposts;
        allpostsize += members[i].totpostsize;
      } else {
        fprintf(fp,lformat,members[i].name,s,members[i].numposts,members[i].totpostsize/members[i].numposts,prtdate(&(members[i].lastpostdate)));
        fprintf(fp,"\n");
      }
    }

  if (lformat[0]=='\0') { /* print footer info */
      fprintf(fp,"=============================== =============== ===== ===== ==================\n");
      fprintf(fp,"Participant Count: %3i                          %5i %5i\n\n",nummembers,allposts,allpostsize/allposts);
    }
  if (fp!=stdout) fclose(fp);

  free(members);
  return 0;
  }


short readmsg(char *msgpath, char *from, struct t_date *datetime, struct t_fido *origin, int *size) {
  /* reads in a message and returns all the info from it */
  FILE *fp;
  char buffer[300];
  int i;

  origin->zone=1;
  origin->point=0;

  fp=fopen_or_die(msgpath,"rb","Could not open message file\n",20);
  fread(from,1,36,fp);
  /* one way to screen most invalidly-formatted message files is to only
  count those where the first letter of the "from" is a letter */
  if ((*from<'a' || *from>'z') && (*from<'A' || *from>'Z')) {
      fclose(fp);
      return FALSE;
    }
  fread(buffer,1,36+72,fp); /* throw away "to" and "subj" */
  fread(buffer,1,20,fp);    /* read the date then convert it */
  cvtdate(datetime,buffer);
  fread(buffer,1,26,fp);    /* read the rest of the buffer */
  origin->node= 256 * buffer[ 4] + buffer[ 5];
  origin->net = 256 * buffer[ 8] + buffer[ 9];

  while (readkluge(buffer,fp)!=NULL) {
    if (!strnicmp(buffer,"FMPT",4)) {
        origin->point=atoi(&(buffer[5]));
      }
    if (!strnicmp(buffer,"INTL",4)) {
        i=instr(buffer,' ',6);
        if (i!=0) origin->zone=atoi(&(buffer[i+1]));
      }
    if (!strnicmp(buffer,"MSGID:",6)) {
        origin->zone=atoi(&(buffer[7]));
        i=instr(buffer,'.',6);
        if (i!=0) origin->point=atoi(&(buffer[i+1]));
      }
    }
  *size=ftell(fp)-190;
  makesure(!fseek(fp,190,SEEK_SET),"Error: couldn't rewind message file\n",20);
  while (fgetcr(buffer,255,fp) && strnicmp(buffer,ORIGINLINE,ORIGINLEN))
    ;
  if (!strnicmp(buffer,ORIGINLINE,ORIGINLEN)) {
      i=rev_instr(buffer,':',strlen(buffer));
      if (i!=0) {
          parse_fido(&(buffer[i-1]),origin);
        }
    }

  fclose(fp);
  return TRUE;
  }


int find_member(struct t_member *members, int *nummembers, char *from) {
  /* find a member in the array, if she's not there create her */
  int i;
  for (i=0;i<*nummembers && stricmp(from,members[i].name);i++)
    ;
  if (i==*nummembers) {
      if (i>=MAXMEMBERS) {
          fprintf(stderr,"\x1b[0;0H\x1b[2J\x07\x07Warning: over %i members found!\n\n",MAXMEMBERS);
          exit(20);
        }
      strcpy(members[*nummembers].name,from);
      members[*nummembers].lastpostdate.year=0;
      members[*nummembers].lastpostdate.month=0;
      members[*nummembers].lastpostdate.day=0;
      members[*nummembers].lastpostdate.hour=0;
      members[*nummembers].lastpostdate.minute=0;
      members[*nummembers].lastpostdate.second=0;
      members[*nummembers].numposts=0;
      members[*nummembers].totpostsize=0;
      (*nummembers)++;
    }
  return i;
  }


/**** The following support functions are used to parse a Fido message ****/

short readkluge(char *buf, FILE *fp) {
  /* read lines until you find a kluge or EOF; return 0 for EOF, 1 for success */
  char c;
  if ((c=getc(fp))==EOF) return 0;
  while (c!='\x01') { /* read a line, maybe the next line will be a kluge */
    while ((c=getc(fp))!='\x0d')
      if (c==EOF) return 0;
    if ((c=getc(fp))==EOF) return 0;
    }
  /* so by now we're just past the ^A of a kluge line.  If we ran out of
  post before this we'd have returned 0 and not have gotten here. */
  while ((c=getc(fp))!='\x0d' && c!=EOF)
    *buf++=c;
  *buf='\0';
  return 1;
  }


short instr(char *s, char find, short start) {
  /* searches within a string for another string */
  short i=start;
  while (s[i]!=find && s[i]!='\0')
    i++;
  if (s[i]=='\0') return 0;
  return i;
  }


short rev_instr(char *s, char find, short start) {
  /* like instr but searches backwards from the end of the string */
  short i=start;
  while (i>=0 && s[i]!=find)
    i--;
  if (i==0) return 0;
  return i;
  }


int find_char(char tofind, char *s) {
  /* find a single character in a string */
  int i=0;
  while (s[i]!='\0' && s[i]!=tofind) i++;
  if (s[i]=='\0')
      return -1;
    else
      return i;
  }


void parse_fido(char *s, struct t_fido *fido) {
  /* parse a fido address in a string into a struct t_fido */
  char addrss[256],*sofar;
  int i;

  strcpy(addrss,s);
  sofar=addrss;

  if ((i=find_char(':',sofar))==-1) {
      fido->zone=1;
      }
    else {
      sofar[i]='\0';
      fido->zone=atoi(sofar);
      sofar=&(sofar[i+1]);
      }
  if ((i=find_char('/',sofar))==-1) {
      /* Improperly formed address */
      return;
      }
    else {
      sofar[i]='\0';
      fido->net=atoi(sofar);
      sofar=&(sofar[i+1]);
      }
  if ((i=find_char('.',sofar))==-1) {
      fido->node=atoi(sofar);
      fido->point=0;
      }
    else {
      sofar[i]='\0';
      fido->node=atoi(sofar);
      fido->point=atoi(&(sofar[i+1]));
      }
  }


short fgetcr(char *buf, int maxlen, FILE *fp) {
/* like fgets() but reads up to the next CR not LF */
  int i=0;
  char c;
  if (feof(fp)) return FALSE;
  while (i<maxlen-1 && (c=getc(fp))!=EOF && c!='\x0d') {
    if (c!='\x0a' && c!='\x8d') buf[i++]=c;
    }
  buf[i]='\0';
  return TRUE;
  }



/**** These functions work with the t_date structure ****/

void cvtdate(struct t_date *date, char *s) {
  /* converts a date in either SEAdog or Fido_11w format into a t_date */
  if (*s>='0' && *s<='9') {            /* it's a Fido_11w format */
      date->year   = atoi(&(s[7]));
      date->month  = mnumber(&(s[3]));
      date->day    = atoi(s);
      date->hour   = atoi(&(s[11]));
      date->minute = atoi(&(s[14]));
      date->second = atoi(&(s[17]));
    } else {                           /* it's a SEAdog format */
      date->year   = atoi(&(s[11]));
      date->month  = mnumber(&(s[7]));
      date->day    = atoi(&(s[4]));
      date->hour   = atoi(&(s[14]));
      date->minute = atoi(&(s[17]));
      date->second = 0;
    }
  }


char *prtdate(struct t_date *date) {
  /* returns a display format of the date in a static string */
  static char datebuf[25], mname[4];
  switch(date->month) {
    case  1: strcpy(mname,"Jan"); break;
    case  2: strcpy(mname,"Feb"); break;
    case  3: strcpy(mname,"Mar"); break;
    case  4: strcpy(mname,"Apr"); break;
    case  5: strcpy(mname,"May"); break;
    case  6: strcpy(mname,"Jun"); break;
    case  7: strcpy(mname,"Jul"); break;
    case  8: strcpy(mname,"Aug"); break;
    case  9: strcpy(mname,"Sep"); break;
    case 10: strcpy(mname,"Oct"); break;
    case 11: strcpy(mname,"Nov"); break;
    case 12: strcpy(mname,"Dec"); break;
    default: strcpy(mname,"???"); break;
    }
  sprintf(datebuf,"%02i %3s %02i %02i:%02i:%02i",date->day,mname,date->year,date->hour,date->minute,date->second);
  return(datebuf);
  }


short cmpdate(struct t_date *d1, struct t_date *d2) {
  /* compares two t_dates, returns -1 if d1<d2, 0 if d1=d2, 1 if d1>d2 where > means later */
  if (d1->year   > d2->year  ) return 1;
  if (d1->year   < d2->year  ) return -1;
  if (d1->month  > d2->month ) return 1;
  if (d1->month  < d2->month ) return -1;
  if (d1->day    > d2->day   ) return 1;
  if (d1->day    < d2->day   ) return -1;
  if (d1->hour   > d2->hour  ) return 1;
  if (d1->hour   < d2->hour  ) return -1;
  if (d1->minute > d2->minute) return 1;
  if (d1->minute < d2->minute) return -1;
  if (d1->second > d2->second) return 1;
  if (d1->second < d2->second) return -1;
  return 0;
  }


void cpydate(struct t_date *dest, struct t_date *src) {
  /* copy a t_date onto another */
  dest->year   = src->year;
  dest->month  = src->month;
  dest->day    = src->day;
  dest->hour   = src->hour;
  dest->minute = src->minute;
  dest->second = src->second;
  }


void tm2date(struct t_date *dest, time_t *t) {
  /* convert a time_t into a t_date */
  struct tm *tp=localtime(t);
  char s[256];
  strftime(s,20,"%d %b %y %H:%M:%S",tp);
  dest->year   = tp->tm_year;
  dest->month  = tp->tm_mon+1;
  dest->day    = tp->tm_mday;
  dest->hour   = tp->tm_hour;
  dest->minute = tp->tm_min;
  dest->second = tp->tm_sec;
  }


int mnumber(char *s) {
  /* returns 1-12 (0 for error) based on next 3 chars being an abbrev of a month */
  if (!strnicmp(s,"Jan",3)) return 1;
  if (!strnicmp(s,"Feb",3)) return 2;
  if (!strnicmp(s,"Mar",3)) return 3;
  if (!strnicmp(s,"Apr",3)) return 4;
  if (!strnicmp(s,"May",3)) return 5;
  if (!strnicmp(s,"Jun",3)) return 6;
  if (!strnicmp(s,"Jul",3)) return 7;
  if (!strnicmp(s,"Aug",3)) return 8;
  if (!strnicmp(s,"Sep",3)) return 9;
  if (!strnicmp(s,"Oct",3)) return 10;
  if (!strnicmp(s,"Nov",3)) return 11;
  if (!strnicmp(s,"Dec",3)) return 12;
  return 0;
  }


/**** Compare functions used by qsort() ****/
/* they all have to be #ifdef'd because MS_DOS compilers can't handle an implicit
   cast from const void * to some actual pointer type.  */

#ifdef AMIGA

int sortcmp_name(struct t_member *arg1, struct t_member *arg2) {
  char *s1,*s2;
  s1=&(arg1->name[strlen(arg1->name)]);
  while (s1>arg1->name && *s1!=' ') s1--;
  s2=&(arg2->name[strlen(arg2->name)]);
  while (s2>arg2->name && *s2!=' ') s2--;
  return (stricmp(s1,s2));
  }


int sortcmp_post(struct t_member *arg1, struct t_member *arg2) {
  if (arg1->numposts > arg2->numposts) return -1;
  if (arg1->numposts < arg2->numposts) return 1;
  return 0;
  }


int sortcmp_date(struct t_member *arg1, struct t_member *arg2) {
  return (cmpdate(&(arg1->lastpostdate),&(arg2->lastpostdate)));
  }


int sortcmp_size(struct t_member *arg1, struct t_member *arg2) {
  if ((arg1->totpostsize/arg1->numposts) > (arg2->totpostsize/arg2->numposts)) return -1;
  if ((arg1->totpostsize/arg1->numposts) < (arg2->totpostsize/arg2->numposts)) return 1;
  return 0;
  }

#endif


#ifdef MS_DOS

int sortcmp_name(const void *arg1, const void *arg2) {
  char *s1,*s2;
  s1 = &((((struct t_member *)arg1)->name[strlen(((struct t_member *)arg1)->name)]));
  while (s1 > ((struct t_member *)arg1)->name && *s1 != ' ') s1--;
  s2 = &((((struct t_member *)arg2)->name[strlen(((struct t_member *)arg2)->name)]));
  while (s2 > ((struct t_member *)arg2)->name && *s2 != ' ') s2--;
  return (stricmp(s1,s2));
  }


int sortcmp_post(const void *arg1, const void *arg2) {
  if (((struct t_member *)arg1)->numposts > ((struct t_member *)arg2)->numposts) return -1;
  if (((struct t_member *)arg1)->numposts < ((struct t_member *)arg2)->numposts) return 1;
  return 0;
  }


int sortcmp_date(const void *arg1, const void *arg2) {
  return (cmpdate(&(((struct t_member *)arg1)->lastpostdate), &(((struct t_member *)arg2)->lastpostdate)));
  }


int sortcmp_size(const void *arg1, const void *arg2) {
  if ((((struct t_member *)arg1)->totpostsize / ((struct t_member *)arg1)->numposts) >
     (((struct t_member *)arg2)->totpostsize / ((struct t_member *)arg2)->numposts))
      return -1;
  if ((((struct t_member *)arg1)->totpostsize / ((struct t_member *)arg1)->numposts) <
     (((struct t_member *)arg2)->totpostsize / ((struct t_member *)arg2)->numposts))
      return 1;
  return 0;
  }


#endif

