/*This line lets emacs recognize this as -*- C -*- Code
 *-----------------------------------------------------------------------------
 *
 * Project:	Tcl Modules
 * Created:	91/10/23
 * Author:	John L. Furlani<john.furlani@East.Sun.COM>
 *
 * Description:
 *      Contains the routines which locate the actual modulefile given a
 *  modulefilename by looking in all of the paths in MODULEPATH. 
 *	
 * $Log: LocateModule.c,v $
 *
 *
 * Revision 1.6  1993/02/04  21:54:32  jlf
 * Fixed a bug with the .version reading and the recursive directory
 * locating.
 *
 * Revision 1.5  1993/01/29  06:06:37  jlf
 * Moved the check for ~, ., and the magic cookie into SortedDirList.
 *
 * Revision 1.4  1993/01/23  01:01:23  jlf
 * Fixed a number of memory leaks and large static arrays.
 *
 * Revision 1.3  1993/01/20  03:28:43  jlf
 * Changed to use given data space when returning paths instead of creating
 * its own and modifying or returning pointers.
 *
 * Revision 1.2  1992/11/19  04:50:29  jlf
 * Fixed a bug when locating modulefiles in a directory.  The magic header
 * of the file was not being checked.  In addition, if the filelist[] had
 * a directory in it, it would not be searched.  Now, the whole filelist[]
 * is searched until I find a true modulefile or another directory to search.
 *
 * Revision 1.1  1992/11/05  23:27:24  jlf
 * Initial revision
 *
 *---------------------------------------------------------------------------*/
static char Id[] =
    "$Id: LocateModule.c,v 2.0 1993/02/20 23:59:32 jlf Exp jlf $";

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

/*
 *  This is a reverse compare function to reverse the filename list.
 */
static int  filename_compare(const void* fi1, const void* fi2)
{
    return strcmp(*(char**)fi2, *(char**)fi1);
}

/*------------------------------------------------------------------------
*
*  Function:	Locate_ModuleFile()
*  Created:	92/06/20
*  RespEngr:	John L. Furlani
*
*  Description:
*	Searches for a modulefile given a string argument which is
*	either a full path or a modulefile name -- usually the
*	argument the user gave.  If it's not a full path, the
*	directories in the MODULESPATH environment variable are
*	searched to find a match for the given name.
*	
*  Parameters:
*	
*	
*  Returns:
*       copies full path into data pointed to filaname
*	
*  Side Effects:
*	newname is set pointing to malloc'd data containing the
*	modulefiles' real name
*	
*  Notes:
*	
*	
*  Deficiencies/ToDo:
*	
*	
*-----------------------------------------------------------------------*/
int
Locate_ModuleFile(Tcl_Interp* interp,
                  char*       modulename,
                  char*       realname,
                  char*       filename)
{
    char*  located_path = NULL;
    char*  modulespath = getenv("MODULEPATH");
    char*  p;
    char*  newname = NULL;
    char** pathlist;
    int    numpaths, i;

    /*
     *  If it is a full path name, that's the module file to load.
     */
    if(modulename[0] == '/' || modulename[0] == '.') {
	p = (char*) strrchr(modulename, '/');
	*p = '\0';
	
	if((newname = GetModuleName(interp, modulename, (p + 1))) == NULL)
	    return TCL_ERROR;
	
	if(!strcmp((p + 1), newname)) {
	    *p = '/';
	    strcpy(filename, modulename);
	} else {
	    sprintf(filename, "%s/%s", modulename, newname);
	    *p = '/';
	}
	strcpy(realname, newname);

        free(newname);
	return TCL_OK;
    }

    /*
     *  If I don't find a path in MODULEPATH, there's nothing to search.
     */
    if(! modulespath) {
	current_module = NULL;
	return TCL_ERROR;
    }

    /*
     *  Split up the MODULEPATH values into multiple directories
     */
    if((pathlist = SplitIntoList(interp, modulespath, &numpaths)) == NULL)
	return TCL_ERROR;

    /*
     *  Check each directory to see if it contains the module
     */
    for(i=0; i<numpaths; i++) {
	newname = GetModuleName(interp, pathlist[i], modulename);
	
	if(newname == NULL) {
            continue;
        } else {
            sprintf(filename, "%s/%s", pathlist[i], newname);

            /*
             *  If it's really a modulefile, then we've found it, otherwise,
             *  we'll keep looking if its a directory.
             */
            if(check_magic(filename, MODULES_MAGIC_COOKIE,
                           MODULES_MAGIC_COOKIE_LENGTH))
                break;
            else
                modulename = newname;
        }
    }

    /*
     *  Free the memory created from the call to SplitIntoList()
     */
    FreeList(pathlist, numpaths);

    /*
     *  If newname still NULL, then we really never found it and we should
     *  return ERROR and clear the full_path array for cleanliness.
     */
    if(newname == NULL) {
        filename[0] = '\0';
        return TCL_ERROR;
    }
	
    strcpy(realname, newname);
    free(newname);
    return TCL_OK;
}

/*------------------------------------------------------------------------
*
*  Function:	GetModuleName
*  Created:	92/06/20
*  RespEngr:	John L. Furlani
*
*  Description:
*	Given a path and a module filename, this function checks to
*	find the modulefile.
*	
*  Parameters:
*	
*	
*  Returns:
*	
*	
*  Side Effects:
*	
*	
*  Notes:
*	
*	
*  Deficiencies/ToDo:
*	
*-----------------------------------------------------------------------*/
char*  GetModuleName(Tcl_Interp* interp,
		     char*       path,
		     char*       modulename)
{
    struct stat    stats;
    char*          fullpath = NULL;
    char*          newname = NULL;
    char**         filelist = NULL;
    int            i, numlist;
    
    if((filelist = SortedDirList(interp, path, modulename, &numlist)) == NULL)
	return NULL;

    if((fullpath = (char*) malloc(1024)) == NULL) {
	Tcl_AppendResult(interp, "The malloc() failed in GetModuleName()", NULL);
	return NULL;
    }

    /*
     *  If there is more than a single entry -- usually means a directory --
     *  then we'll check for a .version file.
     *
     *  The .version file is executed as if it was prepended to the modulefile
     *  it designates as the version for the directory.  The variable
     *  "ModulesVersion" is to be set by the user code in the .version file
     *  which specifies the modulefile in that directory to be used.
     *
     */
    if(numlist > 1) {
        struct stat tmpstat;
	sprintf(fullpath, "%s/%s/.version", path, modulename);

        if(!stat(fullpath, &tmpstat)) {
            char* version;
            int   result;

            /*
             *  Check the .version file to see which modulefile
             *  should really be loaded.
             */
            result = Execute_TclFile(interp, fullpath);

            if(result == TCL_ERROR) {
                goto exit;
            }

            /*
             *  If the user set "ModulesVersion", then we'll use
             *  whatever is in that variable as the new modulename
             */
            if(version = Tcl_GetVar(interp, "ModulesVersion", 0)) {
                sprintf(fullpath, "%s/%s", modulename, version);
                newname = strdup(fullpath);
            }
        }
    }

 exit:
    /*
     *  If newname wasn't set via a .version, select the fisrt one on
     *  the list which is either a modulefile or another directory.  We start
     *  at the highest lexicographical name in the directory since the
     *  filelist is reverse sorted.  If it's a directory, we delve into it.
     */
    if(newname == NULL) {
        int i;
        for(i=0; i<numlist && newname==NULL; i++) {
            sprintf(fullpath, "%s/%s", path, filelist[i]);
            if((stat(fullpath, &stats) == 0) && S_ISDIR(stats.st_mode)) {
                char* tmpname = GetModuleName(interp, path, filelist[i]);
	    
                free((void*) newname);
                newname = tmpname;
            } else if(check_magic(fullpath, MODULES_MAGIC_COOKIE, 
                                  MODULES_MAGIC_COOKIE_LENGTH)) {
                if((newname = strdup(filelist[i])) == NULL) {
                    Tcl_AppendResult(interp, "The strdup() failed in GetModuleName()", 
                                     NULL);
                    return NULL;
                }
            }
        }
    } else {
        /*
         *  We'll check to see if the new name that was found is really
         *  just another directory.  If it is another directory, then we
         *  will recurse until we have found a true modulefile that can be
         *  read -- or possibly none at all.
         */
        sprintf(fullpath, "%s/%s", path, newname);
	
        if((stat(fullpath, &stats) == 0) && S_ISDIR(stats.st_mode)) {
            char* tmpname = GetModuleName(interp, path, newname);
	    
            free((void*) newname);
            newname = tmpname;
        }
    }

    if(fullpath != NULL) free((void*) fullpath);
    FreeList(filelist, numlist);
    
    return newname;
}


/*------------------------------------------------------------------------
*
*  Function:	SortedDirList
*  Created:	92/06/20
*  RespEngr:	John L. Furlani
*
*  Description:
*	Checks the given path for the given modulefile.  If the path +
*	the module filename  is the modulefile, then it is returned as
*	the first element in the list.  If the path + the module
*	filename is a directory, the directory is read as the list.
*	
*  Parameters:
*	
*	
*  Returns:
*	
*	
*  Side Effects:
*	
*	
*  Notes:
*	
*	
*  Deficiencies/ToDo:
*	Make this recursive.
*	
*-----------------------------------------------------------------------*/
char** SortedDirList(Tcl_Interp* interp,
		     char* path,
		     char* modulename,
		     int* listcnt)
{
    struct dirent* file;
    struct stat    stats;
    DIR*           subdirp;
    char*          full_path;
    char**         filelist;
    int            i, j, n, pathlen;
    
    pathlen = strlen(path) + strlen(modulename) + 1;
    if((full_path = (char*) malloc((pathlen+1)*sizeof(char))) == NULL) {
	Tcl_AppendResult(interp, 
                         "The malloc() failed in SortedDirList()", NULL);
	return NULL;
    }
    
    if((filelist = (char**) calloc(n = 100, sizeof(char*))) == NULL) {
	Tcl_AppendResult(interp, 
                         "The calloc() failed in SortDirList().", NULL);
	free((void*)full_path);
	return NULL;
    }
    
    sprintf(full_path, "%s/%s", path, modulename);
    
    if(stat(full_path, &stats) == 0) {
	if(S_ISREG(stats.st_mode)) {
	    *listcnt = 1;
	    filelist[0] = strdup(modulename);
	    free((void*)full_path);

	    return filelist;
	}
    } else {
	FreeList(filelist, n);
	free((void*)full_path);
	return NULL;
    }
    
    if(S_ISDIR(stats.st_mode)) {
        char* buf;
        char* mpath;

	if((subdirp = opendir(full_path)) == NULL) {
	    FreeList(filelist, n);
	    free(full_path);
	    return NULL;
	}

        if((buf = (char*)malloc(8192*sizeof(char))) == NULL) {
            Tcl_AppendResult(interp, 
                             "The buf malloc() failed in SortDirList().",
                             NULL);
            free(full_path);
            return NULL;
        }
        strcpy(buf, full_path);
        buf[pathlen] = '/';
        mpath = (buf + pathlen + 1);

	for(file = readdir(subdirp), i = 0, j = 0; file != NULL;
	    file = readdir(subdirp), i++) {
	    if(j == n) {
		if((filelist = (char**) realloc((char*) filelist, 
						n *= 2)) == NULL) {
		    Tcl_AppendResult(interp, 
				     "The realloc() failed in SortDirList().",
				     NULL);
		    free(full_path);
                    free(buf);
		    return NULL;
		}
	    }

	    if(file->d_name                && 
               *file->d_name != '.'        && 
               strcmp(file->d_name, "..")  &&
               file->d_name[strlen(file->d_name) - 1] != '~') {
                strcpy(mpath, file->d_name);
                if(check_magic(buf, MODULES_MAGIC_COOKIE, 
                               MODULES_MAGIC_COOKIE_LENGTH)) {
                    filelist[j] = (char*) malloc(strlen(modulename) + 
                                                 strlen(file->d_name) + 2);
                    sprintf(filelist[j++], "%s/%s", modulename, file->d_name);
                } else {
                    struct stat mystats;
                    if(stat(buf, &mystats) == 0 && S_ISDIR(stats.st_mode)) {
                        filelist[j] = (char*) malloc(strlen(modulename) + 
                                                     strlen(file->d_name) + 2);
                        sprintf(filelist[j++], "%s/%s", modulename, file->d_name);
                    }
                }
	    }
	}
        filelist[j] = NULL;
	
	qsort((void*) filelist, (size_t) j, 
	      sizeof(char*), filename_compare);
	
	*listcnt = j;
	
	free((void*)full_path);
	closedir(subdirp);

        return filelist;
    }

    FreeList(filelist, n);
    free((void*)full_path);
    return NULL;
}

/*------------------------------------------------------------------------
*
*  Function:	SplitIntoList
*  Created:	92/06/20
*  RespEngr:	John L. Furlani
*
*  Description:
*	Splits a path-type environment variable into an array of char* list.
*	
*  Parameters:
*	
*	
*  Returns:
*	
*	
*  Side Effects:
*	Allocates space for the returned list of paths -- memory must
*	be free'd outside of this function.
*	
*  Notes:
*	
*	
*  Deficiencies/ToDo:
*	
*	
*-----------------------------------------------------------------------*/
char** SplitIntoList(Tcl_Interp* interp,
		     char* pathenv, 
		     int* numpaths) 
{
    char**  pathlist = NULL;
    char*   givenpath = NULL;
    char*   dirname = NULL;
    int     i, n;

    /*
     *  Allocate space to copy in the value of the path value to
     *  split.  The copy is made so a s
     */
    if((givenpath = (char*) malloc(strlen(pathenv) + 1)) == NULL) {
	Tcl_AppendResult(interp, "The malloc() failed in SplitIntoList().",
			 NULL);
	goto exit;
    }

    /*
     *  Allocate the list which is an array of char*.  n is used to
     *  manage the array's growth if there are more than 100 paths in
     *  the list.
     */
    if((pathlist = (char**) calloc(n = 100, sizeof(char*))) == NULL) {
	Tcl_AppendResult(interp, "The calloc() failed in SplitIntoList().",
			 NULL);
	goto exit;
    }

    if(pathenv != NULL) {
	strcpy(givenpath, pathenv);
    } else {
	Tcl_AppendResult(interp, "Internal error in SplitIntoList().",
			 NULL);
	goto exit;
    }

    /*
     *  Split the given path environment variable into its components.
     */
    dirname = strtok(givenpath, ": ");
    for(i=0; dirname; dirname = strtok(NULL, ": ")) {
	if(i == n) {
	    if((pathlist = (char**) realloc((char*) pathlist, 
					    n *= 2)) == NULL) {
		Tcl_AppendResult(interp, 
				 "The realloc() failed in SplitIntoList().",
				 NULL);
		return NULL;
	    }
	}

	pathlist[i++] = strdup(dirname);
    }
    
    *numpaths = i;

 exit:
    if(givenpath != NULL) free((void*)givenpath);
    
    return pathlist;
}

/*------------------------------------------------------------------------
*
*  Function:	FreeList
*  Created:	92/06/20
*  RespEngr:	John L. Furlani
*
*  Description:
*	Frees an char* array type list.
*	
*	
*  Parameters:
*	
*	
*  Returns:
*	
*	
*  Side Effects:
*	The given char** can not be referenced after this call because
*	all the memory associated with it will be freed.
*	
*  Notes:
*	
*	
*  Deficiencies/ToDo:
*	
*	
*-----------------------------------------------------------------------*/
#ifndef FreeList
void FreeList(char** list, int numelem)
{
    register int j;
    
    if(!list) return;
    
    for(j = 0; j < numelem; j++)
	if(list[j] != NULL)
	    free((void*) list[j]);
    
    free((void*) list);
}
#endif
