/*This line lets emacs recognize this as -*- C -*- Code
 *-----------------------------------------------------------------------------
 *
 * Project:	Tcl Modules
 * Created:	92/06/20
 * Author:	John L. Furlani<john.furlani@East.Sun.COM>
 *
 * Description:
 *	This module command prints out the modulefiles that are available in
 *  the directories listed in the MODULEPATH environment variable.
 *	
 * $Log: ModuleCmd_Avail.c,v $
 *
 *
 * Revision 1.11  1993/02/10  16:56:57  jlf
 * Added the inclusion of sys/types.h before sys/stat.h.  Some systems require
 * that this be included before sys/stat.h.
 *
 * Revision 1.10  1993/01/29  06:11:52  jlf
 * Modified print_aligned_files to indicate to print_spaced_files NOT
 * to pad with spaces if it's the last column.
 *
 * Revision 1.9  1993/01/28  16:24:27  jlf
 * Fixed the open/close problem when the cache is used.  When testing all
 * of the directories in the cachedir, the files were being opened and
 * never closed.  Since the directory list can be very long, this caused
 * problems with running out of file descriptors.  I also removed extra
 * tests for access/existence that were not necessary.
 *
 * Added the ability to specify a full pathname which will cause the avail
 * command to just display the modulefiles from that directory.
 *
 * Revision 1.8  1993/01/26  22:33:12  jlf
 * Closed a couple of un-closed files.  Updated the cache version string.
 *
 * Revision 1.7  1993/01/25  20:23:50  jlf
 * Patched up a couple of mem leaks.
 *
 * Revision 1.6  1993/01/25  18:51:18  jlf
 * Fixed a read free'd memory bug introduced when putting the long output
 * back into the avail command.
 *
 * Revision 1.5  1993/01/25  04:18:45  jlf
 * Changed back to print out directory names too.
 *
 * Revision 1.4  1993/01/23  01:02:08  jlf
 * Fixed a number of free'd memory reads.  Cleaned up big static arrays.
 * Changed the avail command to not print out directory names -- just
 * real modulefiles.
 *
 * Revision 1.3  1993/01/20  03:26:25  jlf
 * Revamped the formatted output so that files will be listed sorted in
 * rows top to bottom instead of across.  Fixed a number of bugs with
 * the new caching scheme.
 *
 * Revision 1.2  1993/01/15  21:14:58  jlf
 * Changed the avail cache to just be a list of files.  Then, the output
 * is formatted to the size of the screen when the cache is actually
 * used and not earlier.
 *
 * Revision 1.1  1992/11/05  23:26:59  jlf
 * Initial revision
 *
 *---------------------------------------------------------------------------*/
static char Id[] = 
    "$Id: ModuleCmd_Avail.c,v 2.0 1993/02/20 23:59:40 jlf Exp jlf $";

#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include "global.h"

static char* buf = NULL;
static char* namebuf = NULL;

int
ModuleCmd_Avail(Tcl_Interp*  interp,
                char*        argv[])
{
    char   full_path[1024];
    DIR*   dirp;
    char*  dirname;
    char*  tmppath = getenv("MODULEPATH");
    char*  modpath;
    
    if(! tmppath) {
	Tcl_AppendResult(interp, "MODULEPATH is not set", NULL);
	return TCL_ERROR;
    }
    
    if((modpath = (char*) malloc(strlen(tmppath) + 1)) == NULL) {
	Tcl_AppendResult(interp, "malloc() failed in Available_Modules()", NULL);
	return TCL_ERROR;
    }
    
    strcpy(modpath, tmppath);
    
    if((buf = (char*)malloc(8192*sizeof(char))) == NULL) {
        fprintf(stderr, "Couldn't malloc a buf in ModuleCmd_Avail()\n");
        exit(1);
    }

#ifdef CACHE_AVAIL
    if((namebuf = (char*)malloc(1024*sizeof(char))) == NULL) {
        fprintf(stderr, "Couldn't malloc a namebuf in ModuleCmd_Avail()\n");
        exit(1);
    }
#endif

    /*
     *  If we're given a full-path, then we'll just check that directory.
     *  Otherwise, we'll check every directory in MODULESPATH.
     */
    if(argv[0] && *argv[0] == '/') {
        dirname = argv[0];
	if((dirp = opendir(dirname)) == NULL) {
	    fprintf(stderr, 
                    "\n**** The %s directory can not be opened for reading.\n", 
                    dirname);
            return TCL_ERROR;
	}
        closedir(dirp);
        fprintf(stderr, "\n--------- %s ---------\n", dirname);
        print_dir(dirname);
        fprintf(stderr, "\n");
    } else {
        dirname = strtok(modpath, ":");
        while(dirname) {
            if((dirp = opendir(dirname)) == NULL) {
                fprintf(stderr, 
                        "\n**** The %s directory can not be opened for reading.\n", 
                        dirname);
                dirname = strtok(NULL, ":");
                continue;
            }
            closedir(dirp);
	
            fprintf(stderr, "\n--------- %s ---------\n", dirname);
            if(argv[0]) {  /* show sub directory */
                struct stat stats;
	    
                sprintf(full_path, "%s/%s", dirname, argv[0]);
                if(stat(full_path, &stats) == 0) {
                    if(S_ISDIR(stats.st_mode)) {
                        fprintf(stderr, "Versions of '%s' from '%s':\n\n", 
                                argv[0], dirname, argv[0]);
                        print_dir(full_path);
                    } else {
                        fprintf(stderr, "There is a modulefile named '%s' in '%s'.\n",
                                argv[0], dirname);
                    }
                }
            } else {
                print_dir(dirname);
            }

            fprintf(stderr, "\n");
            dirname = strtok(NULL, ":");
        }
    }

    free(modpath);
    free(buf);
    free(namebuf);
    return TCL_OK;
}  

#define DIREST  50
/*
  Structure to store information about a file.  Includes its name
  and the structure to store information from the stat system call.
*/
typedef struct _file_entry {
    char*        fi_prefix;
    char*        fi_name;
    struct stat  fi_stats;
    int          fi_listcount;
    struct _file_entry* fi_subdir;
} fi_ent;

/*
  Structure for a linked list that stores directories to be listed.
*/
typedef struct _subdir_node {
  fi_ent*              sd_dir;
  struct _subdir_node* sd_next;
} sd_node;

#include <termios.h>
#include <fcntl.h>
#include <unistd.h>

#define  CACHE_VERSION  "v2.0.2"

int    print_dir(char*);
void   chk4spch(char*);
static fi_ent* get_dir(char*, char*, int*, int*);
static void   store_dirlst(FILE*, FILE*, fi_ent*, int, int);
static void   store_files(FILE*, FILE*, fi_ent*, int, int);
static void   print_spaced_file(char*, int, int);
static int    print_aligned_files(char**, int);
static void   store_file(FILE*, fi_ent*);
static void   print_file(FILE*, fi_ent*, int);
static char*  mkdirnm(char*, char*);
static char*  fmtperm(int);
static int    fi_ent_cmp(const void*, const void*);
static char*  storstr(char*);
static void   delete_cache_list(char** list, int count);
static char** create_cache_list(FILE* cacheinput, int* count);
static void   dirlst_to_list(char** list, fi_ent* dirlst_head, 
                      int count, int* beginning);
static void   delete_dirlst(fi_ent* dirlst_head, int count);

int
print_dir(char* dir)
{
    fi_ent*  dirlst_head = NULL;
    int      count = 0;
    int      tcount = 0;
    int      start = 0;
    char**   cache_list;

#ifdef CACHE_AVAIL
    FILE*        fi, *cdir;
    int          usecache=1;
    time_t       dir_time=0, cache_time=0, info_time=0;
    struct stat  dir_stats, cache_stats;

    cache_list = NULL;

    /* ensure any files I create can be read and written by everyone */
    umask(0);

    sprintf(namebuf, "%s/.moduleavailcachedir", dir);
    if((cdir = fopen(namebuf, "r")) != NULL) {
        if(stat(dir, &dir_stats) != -1) {
            if(stat(namebuf, &cache_stats) != -1) {
                dir_time = dir_stats.st_mtime;
                cache_time = cache_stats.st_mtime;
            }
            if(dir_time > cache_time) {
                usecache=0;
            }
        }

        while(usecache && !feof(cdir)) {
            fscanf(cdir, "%s %d\n", buf, &info_time);
            sprintf(namebuf, "%s/%s", dir, buf);
            if(stat(namebuf, &dir_stats) != -1) {
                if(dir_stats.st_mtime <= info_time) {
                    continue;
                }
            }
            usecache=0;
        }
        fclose(cdir);
    } else
        usecache = 0;

    if(usecache) {
        int status;
        sprintf(namebuf, "%s/.moduleavailcache", dir);
        fi = fopen(namebuf, "r");
        cache_list = create_cache_list(fi, &tcount);
        if(cache_list) {
            print_aligned_files(cache_list, tcount);
            delete_cache_list(cache_list, tcount);
            fclose(fi);
            return TCL_OK;
        }
        fclose(fi);
    }

    dirlst_head = get_dir(dir, NULL, &count, &tcount);
    cache_list = (char**)malloc(tcount*sizeof(char**));
    start=0;
    dirlst_to_list(cache_list, dirlst_head, count, &start);

    sprintf(namebuf, "%s/.moduleavailcache", dir);
    if((fi = fopen(namebuf, "w+")) == NULL) {
        print_aligned_files(cache_list, tcount);
        delete_dirlst(dirlst_head, count);
        delete_cache_list(cache_list, start);
    } else {
        sprintf(namebuf, "%s/.moduleavailcachedir", dir);
        if((cdir = fopen(namebuf, "w+")) == NULL) {
            print_aligned_files(cache_list, tcount);
            delete_dirlst(dirlst_head, count);
            delete_cache_list(cache_list, start);
        } else {
            delete_cache_list(cache_list, start);
            store_files(cdir, fi, dirlst_head, count, tcount);
            fflush(fi);
            fclose(cdir);

            fseek(fi,0,0);
            if((cache_list = create_cache_list(fi, &tcount)) == NULL) {
                fprintf(stderr, 
                        "ERROR:  Couldn't load avail cache properly\n");
                exit(1);
            }
            print_aligned_files(cache_list, tcount);
            delete_cache_list(cache_list, tcount);
            fclose(cdir);
        }
        fclose(fi);
    }
#else
    dirlst_head = get_dir(dir, NULL, &count, &tcount);
    cache_list = (char**)malloc(tcount*sizeof(char**));
    start=0;
    dirlst_to_list(cache_list, dirlst_head, count, &start);
    delete_dirlst(dirlst_head, count);
    print_aligned_files(cache_list, tcount);
    delete_cache_list(cache_list, start);
#endif
}


static fi_ent* 
get_dir(char* dir, char* prefix, int* listcount, int* total_count)
{
  struct dirent* dp;
  DIR*           dirptr;
  fi_ent*        dirlst_head;
  fi_ent*        dirlst_cur;
  fi_ent*        dirlst_last;
  char*          dirname;
  int            count = 0, i;

  if(!(dirptr = opendir(dir)))
      return 0;

  if((dirlst_cur = dirlst_head = 
      (fi_ent*) calloc(DIREST, sizeof(fi_ent))) == 0)
    {
      fprintf(stderr, "can't calloc memory\n");
      exit(1);
    }
  dirlst_last = dirlst_head + DIREST;
  
  for(count = 0,  dp = readdir(dirptr); 
      dp != NULL; dp = readdir(dirptr))
    {
        if(*dp->d_name == '.') continue;

        if(dirlst_cur == dirlst_last) {
            dirlst_head = (fi_ent*) realloc((char*) dirlst_head, 
                                            (count<<1)*sizeof(fi_ent));
            dirlst_cur = dirlst_head + count;
            dirlst_last = dirlst_head + (count<<1);
	}

        dirname = mkdirnm(dir, dp->d_name);
        if(stat(dirname, &(dirlst_cur->fi_stats)) < 0) {
            fprintf(stderr, "%s not found.\n", dirname);
            exit(1);
        }

        if(dirlst_cur->fi_stats.st_mode & S_IFDIR) {
            char* np = strdup(mkdirnm(prefix, dp->d_name));
            char* ndir = strdup(mkdirnm(dir, dp->d_name));
            int   tmpcount = 0;

            dirlst_cur->fi_subdir = get_dir(ndir, np, 
                                            &dirlst_cur->fi_listcount, 
                                            &tmpcount);
            /*
             *  Add the number of real modulefiles (i.e. not subdirs and 
             *  not non-modulefiles) to our total number of modulefiles 
             *  contained in the structure.
             */
            (*total_count) += tmpcount;
            /*
             *  This means that it's an empty directory so the prefix is never used
             */
            if(dirlst_cur->fi_listcount == 0) free(np);
            free(ndir);
        } else if(!check_magic(dirname, MODULES_MAGIC_COOKIE, 
                               MODULES_MAGIC_COOKIE_LENGTH)) {
            continue;
        } else {
            dirlst_cur->fi_subdir = NULL;
        }

        if(dp->d_name[strlen(dp->d_name)-1] != '~') {
            dirlst_cur->fi_prefix = prefix;
            dirlst_cur->fi_name   = strdup(dp->d_name);
            count++;
            (*total_count)++;
            dirlst_cur++;
        }
    }

  if(count > 1)
      qsort(dirlst_head, count, sizeof(fi_ent), fi_ent_cmp);

  closedir(dirptr);

  *listcount = count;
  return dirlst_head;
}

static int
compute_maxwidth(char** list, int tcount)
{
    int maxwidth = 0, i;
    for(i=0; i<tcount; i++) {
        int entry_len = strlen(list[i]);

        if(entry_len > maxwidth) 
            maxwidth = entry_len;
    }

    return maxwidth;
}


static int
fi_compute_maxwidth(fi_ent* dirlst_head, int count)
{
    fi_ent* dirlst_cur;
    int     maxwidth = 0, i;

    for(i = 0, dirlst_cur = dirlst_head; i < count && dirlst_cur;
        i++, dirlst_cur++) {
        int entry_len = strlen(dirlst_cur->fi_name);

        if(dirlst_cur->fi_prefix)
            entry_len += strlen(dirlst_cur->fi_prefix) + 1;

        if(dirlst_cur->fi_subdir) {
            entry_len = fi_compute_maxwidth(dirlst_cur->fi_subdir, 
                                          dirlst_cur->fi_listcount);
        }
        if(entry_len > maxwidth) 
            maxwidth = entry_len;
    }

    return maxwidth;
}

static void
dirlst_to_list(char** list, fi_ent* dirlst_head, int count, int* beginning)
{
    fi_ent* dirlst_cur;
    int i;

    for(i = 0, dirlst_cur = dirlst_head; i < count && dirlst_cur;
        i++, dirlst_cur++)
    {
        if(dirlst_cur->fi_prefix) {
            sprintf(buf, "%s/%s", dirlst_cur->fi_prefix, 
                    dirlst_cur->fi_name);
            list[(*beginning)++] = strdup(buf);
        } else {
            list[(*beginning)++] = strdup(dirlst_cur->fi_name);
        }

        if(dirlst_cur->fi_subdir)
            dirlst_to_list(list, dirlst_cur->fi_subdir, 
                           dirlst_cur->fi_listcount, beginning);
    }
}

static void
delete_dirlst(fi_ent* dirlst_head, int count)
{
    fi_ent* dirlst_cur;
    int i;

    for(i = 0, dirlst_cur = dirlst_head; i < count && dirlst_cur;
        i++, dirlst_cur++)
    {
        free(dirlst_cur->fi_name);

        if(dirlst_cur->fi_subdir) {
            delete_dirlst(dirlst_cur->fi_subdir, 
                          dirlst_cur->fi_listcount);
        }
    }
    if(dirlst_head->fi_prefix) free(dirlst_head->fi_prefix);
    free((char*)dirlst_head);
}

static void
store_files(FILE* cacheinfo, FILE* outfile, fi_ent* dirlst_head, 
            int count, int tcount)
{
    int maxwidth = fi_compute_maxwidth(dirlst_head, count) + 2;
    fprintf(outfile, "%s\n", CACHE_VERSION);
    fprintf(outfile, "%d\n", tcount);
    store_dirlst(cacheinfo, outfile, dirlst_head, count, 0);
}

static void
store_dirlst(FILE* cacheinfo, FILE* outfile, fi_ent* dirlst_head, 
             int count, int maxwidth)
{
    fi_ent* dirlst_cur;
    int i;

    for(i = 0, dirlst_cur = dirlst_head; i < count && dirlst_cur;
        i++, dirlst_cur++)
    {
        if(dirlst_cur->fi_stats.st_mode & S_IFDIR) {
            if(dirlst_cur->fi_prefix) {
                fprintf(cacheinfo, "%s/%s %d\n", dirlst_cur->fi_prefix, 
                        dirlst_cur->fi_name, dirlst_cur->fi_stats.st_mtime);
            } else {
                fprintf(cacheinfo, "%s %d\n", dirlst_cur->fi_name, 
                        dirlst_cur->fi_stats.st_mtime);
            }
        }

        store_file(outfile, dirlst_cur);

        if(dirlst_cur->fi_subdir) {
            store_dirlst(cacheinfo, outfile, dirlst_cur->fi_subdir, 
                         dirlst_cur->fi_listcount, maxwidth);
        }
    }
    if(dirlst_head->fi_prefix) free(dirlst_head->fi_prefix);
    free((char*)dirlst_head);
}

static void
store_file(FILE* outfile, fi_ent* file)
{
  int filelen;

  chk4spch(file->fi_name);  /* turn any weird characters into ? marks */

  filelen = strlen(file->fi_name);

  if(file->fi_name[filelen-1] != '~') {
      if(file->fi_prefix) {
          fprintf(outfile, "%s/", file->fi_prefix);
      }
      fprintf(outfile, "%s\n", file->fi_name);
      free(file->fi_name);
  } else {
      free(file->fi_name);
  }
}

static void
print_file(FILE* outfile, fi_ent* file, int maxwidth)
{
  int filelen;

  chk4spch(file->fi_name);  /* turn any weird characters into ? marks */

  filelen = strlen(file->fi_name);

  if(file->fi_name[filelen-1] != '~') {
      if(file->fi_prefix) {
          filelen += (strlen(file->fi_prefix) + 1);
          fprintf(outfile, "%s/", file->fi_prefix);
      }
      fprintf(outfile, "%s", file->fi_name);

      free(file->fi_name);

      while(filelen < maxwidth) {
          filelen++;
          putc(' ', outfile);
      }
  } else {
      free(file->fi_name);
  }
}

static char**
create_cache_list(FILE* cacheinput, int* count)
{
    char**   list;
    int      i;

    fscanf(cacheinput, "%s", buf);
    if(strcmp(buf, CACHE_VERSION)) {
        return NULL;
    }
    
    fscanf(cacheinput, "%d", count);

    list = (char**)malloc(*count*sizeof(char**));
    if(list == NULL) {
        fprintf(stderr, "ERROR: malloc() failed in read_cache_list()\n");
        exit(1);
    }

    for(i=0;i<*count;i++) {
        fscanf(cacheinput, "%s", buf);
        list[i] = strdup(buf);
    }

    return list;
}

static void
delete_cache_list(char** list, int count) {
    int i;

    for(i=0; i<count; i++) {
        free(list[i]);
    }
    free((char*)list);
}


static int
print_aligned_files(char** list, int tcount)
{
    int      i, j, cnt, columns, num_rows, ent;
    int      maxwidth = 0;
    int      terminal_width = 80;
    struct   winsize window_size;

    if(isatty(2))
        if(ioctl(2, TIOCGWINSZ, &window_size) != -1)
            terminal_width = (window_size.ws_col == 0 ? 80 : 
                              window_size.ws_col);

    maxwidth = compute_maxwidth(list, tcount) + 2;
    if((columns = terminal_width/maxwidth) == 0)
        columns = 1;

    num_rows = (tcount-1)/columns+1;

    for(j=0; j<num_rows; j++) {
        for(i=0; i<columns; i++) {
            ent = (num_rows * i) + j;
            if(ent < tcount)
                print_spaced_file(list[ent], maxwidth, i!=columns-1);
        }
        fprintf(stderr, "\n");
    }

    return 1;
}

static void 
print_spaced_file(char* name, int maxwidth, int space)
{
  int filelen;

  chk4spch(name);  /* turn any weird characters into ? marks */

  filelen = strlen(name);

  fprintf(stderr, "%s",  name);
  if(space) {
      while(filelen < maxwidth) {
          filelen++;
          putc(' ', stderr);
      }
  }
}

static
char* mkdirnm(char* dir, char* file)
{
  static char  dirbuf[BUFSIZ];

  if(dir == NULL || *dir == '\0' || *dir == '.')
    return strcpy(dirbuf, file);

  if((int)((strlen(dir)+1) + (strlen(file)+1)) > BUFSIZ)
    {
      fprintf(stderr, "found a filename too long\n");
      exit(1);
    }
  strcpy(dirbuf, dir);
  if(dir[strlen(dir) - 1] != '/' && file[0] != '/')
    strcat(dirbuf, "/");
  return strcat(dirbuf, file);
}

/* ----------------------- CHK4SPCH()

   chk4spch() goes through the given string and changes any non-printable
   characters to question marks.
*/
void chk4spch(char* finame)
{
  for(;*finame;finame++)
    if(!isgraph(*finame)) *finame = '?';

}

/* ----------------------- FI_ENT_CMP()

   fi_ent_cmp() is compares two fi_ents returning a value
     >0 -- if fi2 > fi1
     <0 -- if fi2 < fi1
     =0 -- if fi2 == fi1

   Different cmdline arguments (i.e. -u, -c, -t, -z) will change what
   value is compared.  As a default, the name is used.
*/
static
int fi_ent_cmp(const void* fi1, const void* fi2)
{
  extern int sort_dir;

  return strcmp(((fi_ent*)fi1)->fi_name, 
                ((fi_ent*)fi2)->fi_name);
}
