/*-------------------------------------------------------------*
 | File: TREE.c - After Tomas Rokicki, Fred Fish Disk nr. 306  |
 | Revised: Maurizio LORETI (MLO) - First version 1.00, 911123 |
 | Last revision: version 1.04, MLO 920405                     |
 +-------------------------------------------------------------+
 | For the description, refer to the README file, or to the    |
 | Usage() procedure at the end of this program; a short help  |
 | will be printed giving the command TREE ? , TREE -H or TREE |
 | followed by an invalid option.                              |
 *-------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <exec/types.h>
#include <exec/exec.h>
#include <libraries/dos.h>
#include <proto/exec.h>
#include <proto/dos.h>
#include "pattern.h"

#define SYS_NORMAL_CODE    0     /* Exit codes for the operating system */
#define SYS_ABORT_CODE     1

#define MATCH_STRING       -1    /* Flag for the NamEntry tree */

#define FILENAME_MAX       128   /* Dimension of the file name buffer */
#define OUTLINE_DIM        128   /* Dimension of the printout buffer*/

/**
 | Structures to implement: a linked list of character strings
 | (StringSet); a linked list of directory entries (DirEntry);
 | and a linked list of name patterns (NamEntry).
**/

typedef struct sStringSet {
   struct sStringSet *next;
   char what[1];
} StringSet;

typedef struct sDirEntry {
   struct sDirEntry *next;
   char name[1];
} DirEntry;

typedef struct sNamEntry {
   struct sNamEntry *next;
   LONG type;
   void *pArg;
} NamEntry;

/**
 | Global variables
**/

struct Library *PatternBase = NULL;       /* pattern.library pointer */

struct FileInfoBlock *pFIB = NULL;        /* FileInfoBlock buffer */
int aborted                = FALSE;       /* CTRL-C flag */
int dirWanted              = FALSE;       /* "-x" or "+x" flag */
int namWanted              = FALSE;       /* "-n|N" or "+n|N" flag */
int caseSignificant;                      /* "case is significant" flag */
int fullDirPath;                          /* "-d" or "-D" flag */
StringSet *dirList         = NULL;        /* Subdirectories tree root */
StringSet *extList         = NULL;        /* Extensions tree root */
NamEntry  *namList         = NULL;        /* Name patterns tree root */
char *mFormat              = "%p%s";      /* Default output format */
char outLine[OUTLINE_DIM];                /* Output print buffer */

/**
 | Procedure prototypes
**/

StringSet  *Add(char *s, StringSet *pSS);
NamEntry   *AddNam(char *s, NamEntry *pNE);
void        CheckControlC(void);
int         CheckExt(char *s);
void        Cleanup(int code);
int         CXBRK(void);
int         FileDate(char *pc, struct DateStamp *pDS);
int         FileTime(char *pc, struct DateStamp *pDS);
int         Member(char *s, StringSet *pSS);
int         NamMatch(char *name);
void        NoMem(void);
void        PrintFile(char *s);
char       *StrLast(char *s, char c);
void        Tree(char *s);
void        Usage(void);

void main(
   int argc,
   char *argv[]
){
   char path[FILENAME_MAX] = "";
   register char option = 'F';      /* Default opt for first arg: format */

/**
 | Main program: allocates the internal buffer for the directory
 | chain (pFIB); then scans the input line arguments; then calls
 | Tree() that does the work.
**/

   if ((pFIB = (struct FileInfoBlock *)
            AllocMem(sizeof(struct FileInfoBlock), MEMF_CLEAR)) == NULL) {
      NoMem();
   }

   while (--argc > 0) {
      switch ((*++argv)[0]) {
         case '-':
            switch (option = (*argv)[1]) {
               case 'X':   case 'x':
                  dirWanted = FALSE;
                  break;
               case 'N':
                  caseSignificant = TRUE;
                  namWanted = FALSE;
                  break;
               case 'n':
                  caseSignificant = FALSE;
                  namWanted = FALSE;
                  break;
               case 'D':   case 'd':
               case 'F':   case 'f':
               case 'R':   case 'r':
                  break;
               default:
                  Usage();
                  break;
            }
            break;
         case '+':
            switch (option = (*argv)[1]) {
               case 'X':   case 'x':
                  dirWanted = TRUE;
                  break;
               case 'N':
                  caseSignificant = TRUE;
                  namWanted = TRUE;
                  break;
               case 'n':
                  caseSignificant = FALSE;
                  namWanted = TRUE;
                  break;
               default:
                  Usage();
                  break;
            }
            break;
         case '?':
            Usage();
            break;
         default:
            switch (option) {
               case 'd':
                  fullDirPath = FALSE;
                  dirList = Add(*argv, dirList);
                  break;
               case 'D':
                  fullDirPath = TRUE;
                  dirList = Add(*argv, dirList);
                  break;
               case 'x':   case 'X':
                  extList = Add(*argv, extList);
                  break;
               case 'f':   case 'F':
                  mFormat = *argv;
                  break;
               case 'r':   case 'R':
                  strcpy(path, *argv);
                  option = '\0';
                  break;
               case 'N':   case 'n':
                  namList = AddNam(*argv, namList);
                  break;
               default:             /* Cannot happen */
                  break;
            }
            break;
      }
   }

   Tree(path);

   Cleanup(SYS_NORMAL_CODE);
}

StringSet *Add(
   char *s,
   StringSet *pSS
){
   register StringSet *t;

/**
 | Adds the string "s" in the linked list with root "pSS".
**/

   if ((t = malloc(sizeof(StringSet) + strlen(s))) == NULL)    NoMem();
   t->next = pSS;
   strcpy(t->what, s);

   return t;
}

NamEntry *AddNam(
   char *s,
   NamEntry *pNE
){
   register NamEntry *t;

/**
 | Adds the name pattern "s" in the linked list with root "pNE".
 | The pattern.library is opened at the first entry; and some memory
 | for the new structure is obtained from the heap. If "s" is a real
 | pattern, its "pArg" member is NULL and its "type" member is the
 | PatternHandle (must be positive); if "s" is a simple string,
 | "pArg" points to a cleaned copy of "s" and "type" is negative.
**/

   if (PatternBase == NULL   &&
    (PatternBase = OpenLibrary(PATLIB_NAME, PATLIB_MIN_VERSION)) == NULL) {
      fprintf(stderr, "TREE: couldn't open %s rev. %d\n",
             PATLIB_NAME, PATLIB_MIN_VERSION);
      Cleanup(SYS_ABORT_CODE);
   }

   if ((t = malloc(sizeof(NamEntry))) == NULL)  NoMem();

   if (IsPattern(s, outLine, 0)) {
      t->pArg = NULL;
      if ( (t->type = caseSignificant ?   AllocPattern(s, 0) :
                                          AllocPatternNoCase(s, 0)) < 0) {
         fprintf(stderr, "TREE: %s\n", PatternErrorString(t->type,
                 "default", outLine, OUTLINE_DIM));
         Cleanup(SYS_ABORT_CODE);
      }
   } else {
      t->type = MATCH_STRING;
      if ((t->pArg = malloc(strlen(outLine) + 1)) == NULL) {
         free(t);
         NoMem();
      }
      strcpy(t->pArg, outLine);
   }

   t->next = pNE;
   return t;
}

void CheckControlC(void)
{
   if ((SetSignal(0L, SIGBREAKF_CTRL_C)) & SIGBREAKF_CTRL_C) {
      aborted = TRUE;
   }
}

int CheckExt(
   char *s
){
   register char *pc;

/**
 | Looks for the extension of the filename "s" (the characters after
 | the last '.') and compares the extension with the linked list
 | starting at "extList").
**/

   return ((pc = StrLast(s, '.')) == NULL   ||   !Member(++pc, extList));
}

void Cleanup(
   int code
){
   register StringSet *pS;
   register NamEntry  *pNE;

/**
 | Frees allocated memory, gives back all obtained resources.
**/

   if (pFIB != NULL) FreeMem(pFIB, sizeof(struct FileInfoBlock));

   while ((pS = extList) != NULL) {
      extList = pS->next;
      free(pS);
   }
   while ((pS = dirList) != NULL) {
      dirList = pS->next;
      free(pS);
   }
   while ((pNE = namList) != NULL) {
      namList = pNE->next;
      if (pNE->pArg != NULL)  free(pNE->pArg);
      if (pNE->type >  0)     FreePattern(pNE->type);
      free(pNE);
   }

   if (PatternBase != NULL)   CloseLibrary(PatternBase);

   exit(code);
}

int CXBRK(void)
{
  aborted = TRUE;
  return 0;
}

int FileDate(
   char *pc,
   struct DateStamp *pDS
){
   register int day, month, year;
   static char *mName[] = {
      "Jan", "Feb", "Mar", "Apr", "May", "Jun",
      "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
   };
 
   day    = pDS->ds_Days - 2251;
   year   = (4 * day + 3) / 1461;
   day   -= 1461 * year / 4;
   year  += 1984;
   month  = (5 * day + 2) / 153;
   day    = day - (153 * month + 2) / 5 + 1;
   month += 3;
   if (month > 12) {
      year++;
      month -= 12;
   }

   return sprintf(pc, "%02d-%s-%d", day, mName[--month], year);
}

int FileTime(
   char *pc,
   struct DateStamp *pDS
){
   register int hour, mins, sec;

   sec  = pDS->ds_Tick   / TICKS_PER_SECOND;
   hour = pDS->ds_Minute / 60;
   mins = pDS->ds_Minute % 60;

   return sprintf(pc, "%02d:%02d:%02d", hour, mins, sec);
}

int Member(
   char *s,
   StringSet *pSS
){

/**
 | Looks for the string "s" in the linked list with root "pSS".
 | The comparison is case insensitive (stricmp).
**/

   if (*s) {
      while (pSS != NULL) {
         if (!stricmp(s, pSS->what)) return TRUE;
         pSS = pSS->next;
      }
   }

   return FALSE;
}

int NamMatch(
   char *name
){
   register NamEntry *pNE = namList;

/**
 | Compares the file name ("name") with the known name patterns.
**/

   while (pNE != NULL) {
      if (pNE->type == MATCH_STRING) {
         if (caseSignificant) {
            if (!strcmp(name, pNE->pArg))       return TRUE;
         } else {
            if (!stricmp(name, pNE->pArg))      return TRUE;
         }
      } else {
         register LONG r = MatchThePattern(pNE->type, name);

         if (r < 0) {
            fprintf(stderr, "TREE: %s\n",
                    PatternErrorString(r, "default", outLine, OUTLINE_DIM));
         }
         if (r)   return TRUE;
      }
      pNE = pNE->next;
   }

   return FALSE;
}

void NoMem(void)
{
   fputs("TREE: couldn't obtain memory\n", stderr);
   Cleanup(SYS_ABORT_CODE);
}

void PrintFile(
   char *s
){
   register char *p = outLine;
   register char *q = mFormat;

/**
 | Prints the file name ("s") in the needed format.
**/

   while (*q) {
      if (*q == '%') {
         switch (*++q) {
            case 's':
               {  register char *pc;

                  if ((pc = StrLast(s, '/')) != NULL   ||
                      (pc = StrLast(s, ':')) != NULL) {
                           strcpy(p, ++pc);
                  } else {
                     strcpy(p, s);
                  }
                  p += strlen(p);
                  ++q;
               }
               break;
            case 'p':
               {  register char *pc;
                  register char *ps=s;

                  if ((pc = StrLast(s, '/')) != NULL   ||
                      (pc = StrLast(s, ':')) != NULL) {
                           while (ps <= pc) *p++ = *ps++;
                  }
               }
               ++q;
               break;
            case 'b':
               {  register int i = sprintf(p, "%ld", pFIB->fib_Size);

                  p += i;
               }
               ++q;
               break;
            case 'd':
               p += FileDate(p, &pFIB->fib_Date);
               ++q;
               break;
            case 't':
               p += FileTime(p, &pFIB->fib_Date);
               ++q;
               break;
            case 'q':
               *p++ = '\"';
               ++q;
               break;
            default:
               *p++ = *q++;
               break;
         }
      } else {
         *p++ = *q++;
      }
   }
   *p = '\0';
   puts(outLine);
}

char *StrLast(
   char *s,
   char c
){
   register char *pc = NULL;

/**
 | Returns a pointer to the last occurrence of "c" in the
 | string "s", or NULL.
**/

   while (*s) {
      if (*s == c) pc = s;
      s++;
   }

   return pc;
}

void Tree(
   char *path
){
   BPTR dlock;
   char fileName[FILENAME_MAX];
   register char *pc, *pDir;
   register DirEntry *rdE = NULL;
   register DirEntry *pdE;

/**
 | This procedure checks (recursively) a directory.
 | "path" contains the full directory name; Tree() scans the wanted
 | directory, processing immediately the 'true' files; the local
 | subdirectories (if any) are checked recursively at the end.
 | If an error is detected from Tree(), the directory/file test is
 | stopped and the global flag "aborted" is set; this makes possible, in
 | the further steps, to recursively free() all the memory that has been
 | allocated in order to store subdirectory names.
 |
 | First: obtain a lock on the wanted directory, and Examine() the lock;
 | since only one directory is being scanned at a time, we can use a
 | single FileInfoBlock buffer.
**/

   if ((dlock = Lock(path, ACCESS_READ)) == NULL) return;

   if (Examine(dlock, pFIB)) {

/**
 | Prepare in "fileName" the full directory name - to which local
 | filenames will be appended; and pDir, the pointer to the directory
 | name according to the fullDirPath global flag.
**/

      pc = strcpy(fileName, path);
      pc += strlen(fileName);
      if (pc != fileName   &&   *(pc-1) != ':')    *pc++ = '/';

      pDir = fullDirPath ? fileName : pc;

/**
 | Now, loop over all directory entries. As already said, all the
 | 'real' files are immediately checked; the subdirectory names are
 | stored in a linked list to be examined at the end. This list is
 | implemented as a LIFO tree (the simplest type).
**/

      while (!aborted   &&   ExNext(dlock, pFIB)) {
         (void) strcpy(pc, pFIB->fib_FileName);
         if (pFIB->fib_DirEntryType < 0) {

/**
 | A file. If a name pattern has been given, check this first;
 | then check the extension, if any.
**/

            if (namWanted ^ NamMatch(pc))    continue;
            if (dirWanted ^ CheckExt(pc))    PrintFile(fileName);
         } else {

/**
 | If a memory allocation error is detected when asking space for
 | our linked list, we exit the "while" loop; setting the "aborted"
 | flag before exiting, ensures that all the memory we had from
 | these malloc()'s will later be free()-ed recursively.
**/

            if (!Member(pDir, dirList)) {
               if ((pdE = malloc(sizeof(DirEntry) + strlen(fileName)))
                     == NULL) {
                  aborted = TRUE;
                  break;
               }

               (void) strcpy(pdE->name, fileName);
               pdE->next = rdE;
               rdE = pdE;
            }
         }

         CheckControlC();
      }
   }
   UnLock(dlock);

/**
 | Now, loop over all detected subdirectories (if any);
 | freeing in the same time the memory used to store their names.
**/

   while (rdE != NULL) {
      if (!aborted) Tree(rdE->name);

      free(rdE->name);
      pdE = rdE->next;
      free(rdE);
      rdE = pdE;
   }
}

void Usage(void) {
 char *desc[] = {
  "\nSyntax:\tTREE [opt arg [arg [ ... ]] [opt arg [arg [ ... ]] ...",
  "Lists on stdout the file names in the directory tree. The options are:\n",
  " -r name :    starting directory (default: current working directory).",
  " -d name [name [ ... ]] :        name(s) (case not significant) of the",
  "          directories to be excluded (directory name only, e.g. \"pk\").",
  " -D name [name [ ... ]] :        name(s) (case not significant) of the",
  "           directories to be excluded (full path, e.g. \"work:tex/pk\").",
  " -n pat [pat [ ... ]] :  patterns matching files to be excluded; *OR*,",
  " +n pat [pat [ ... ]] :         patterns matching the only files to be",
  "           listed. If given uppercase (-N,+N) the case is significant.",
  " -x ext [ext [ ... ]] :      extensions of files to be excluded; *OR*,",
  " +x ext [ext [ ... ]] :     extensions of the only files to be listed.",
  " -f \"fmt\" :     output format, between double quotes (\"). Can contain:",
  "    %s - that will be substituted from the file name;",
  "    %p - the file path (e.g. \"ram:\" or \"tex:pk/\");",
  "    %b - the file size (in bytes);",
  "    %d - the file date (e.g. \"08-May-1988\");",
  "    %t - the file time (e.g. \"09:10:11\");",
  "    %q - a double quote character (\");",
  "    %% - the escape character % itself;",
  "    all other characters will copied to stdout.\n",
  NULL
 };
   register char **ppc = desc;

   while (*ppc) {
      fputs(*ppc++, stderr);
      putc('\n',    stderr);
   }

   Cleanup(SYS_NORMAL_CODE);
}
