/************************************************************************
 * This program is Copyright (C) 1986 by Jonathan Payne.  JOVE is	*
 * provided to you without charge, and with no warranty.  You may give	*
 * away copies of JOVE, including sources, provided that this notice is *
 * included in all the files.						*
 ************************************************************************/

#include "jove.h"

RCS("$Id: dirs.c,v 14.31.0.12 1994/06/23 02:46:24 tom Exp tom $")

#include "io.h"

char	*HomeDir;			/* Home directory */
int	HomeLen ZERO;			/* Length of home directory string */

#ifdef CHDIR

#if (NDIRS-0 <= 0)
#   define NDIRS	5
#endif

private char	*dir_stack[NDIRS] ZERO;
private int	DirSP ZERO;		/* Directory stack pointer */

#ifndef CHDIR_EXT			/* Fixed-size stack */
#   define DirStack	dir_stack
#   define DirStkSize	NDIRS
#else					/* Expandable stack */
private char	**DirStack = dir_stack;
private int	DirStkSize = NDIRS;

DEF_INT( "dirs-unique", DUnique, V_BOOL ) _IF(def CHDIR_EXT)_IF(def PRIVATE) ZERO;
DEF_INT( "dirs-extract", DExtract, V_BOOL ) _IF(def CHDIR_EXT)_IF(def PRIVATE) ZERO;

private char	*old_pwd ZERO;		/* Previous working directory */
#endif

#define PWD	(DirStack[DirSP])


/*
 * Change to new working directory.
 * Note that the new working directory's name is NOT recorded in the
 * directory stack yet, since that depends too much on what we're doing.
 * (cd, pushd, or popd)
 */
private void	do_chdir __(( const char *newdir ));
private void
do_chdir(newdir)
register const char	*newdir;
{
	if (chdir(newdir) != 0)
		complain("%f: cannot change into %s.", newdir);
#ifdef CHDIR_EXT
	if (strcmp(PWD, newdir) != 0)
		set_str(&old_pwd, PWD);
#endif
	updmodline();
#ifdef MENUS	/* since this may affect the file names in buffer menu */
	Bufchange++;
#endif
}

#ifdef CHDIR_EXT

/*
 * Expand the directory stack.
 */
private void	dirs_grow __(( void ));
private void
dirs_grow()
{
	register char	**newstack;

	newstack = (char **) emalloc((DirStkSize + NDIRS) * sizeof(char *));
	byte_copy(DirStack, newstack, DirStkSize * sizeof(char *));
	bzero(newstack + DirStkSize, NDIRS * sizeof(char *));
	if (!ISSTATIC(DirStack))
		free(DirStack);
	DirStack = newstack;
	DirStkSize += NDIRS;
}

/*
 * Find a directory in the stack.
 * Returns index of the duplicate in the stack if found, and -1 if missing.
 */
private int	dirs_find __(( const char *_(dir) ));
private int
dirs_find(dir)
const char	*dir;
{
	register char	**dirp = &PWD + 1;

	while (strcmp(dir, *--dirp) != 0) {
		if (dirp == &DirStack[0])
			return -1;
	}
	return (dirp - DirStack);
}

/*
 * Remove an entry from the stack.
 * Note: this should not be the top entry (PWD).
 */
private void	dirs_delete __(( int entry ));
private void
dirs_delete(entry)
{
	register char	**dirp = &DirStack[entry],
			**top = &PWD;

	--DirSP;
	free(dirp[0]);
	while (dirp < top) {
		dirp[0] = dirp[1];
		++dirp;
	}
	dirp[0] = NULL;
}

/*
 * Initialize directory stack and old_pwd, possibly from environment.
 * The environment variable DIRS is supposed to contain the directory stack,
 * as listed with the csh(1) command `dirs' (or, preferably, `dirs -l' for
 * absolute pathnames if your version of csh supports it; even though JOVE
 * can handle leading tildes, many other programs cannot.)
 * This assumes that a valid PWD has already been set up.
 */
public void
dirs_init()
{
	register char	*dir;

	if ((dir = getenv("DIRS")) && *dir) {
		char		dirbuf[FILESIZE];
		register char	*sep;
		register char	**dirp;

		do {
			if (sep = index(dir, ' '))
				*sep++ = '\0';

			if (*dir == '\0')	/* catch leading spaces. */
				continue;

			dir = PathParse(dir, dirbuf);  /* to resolve "~/foo". */

			if (True(DUnique) && dirs_find(dir) >= 0)
				continue;

			if (DirSP == 0 && strcmp(PWD, dir) == 0)
				continue;

			/* check that is is still valid. */
			if (chdir(dir) != 0)
				continue;

			/* Add to the bottom, pushing the existing stack up. */
			if (++DirSP >= DirStkSize)
				dirs_grow();

			for (dirp = &DirStack[DirSP];  dirp > &DirStack[0];  --dirp)
				dirp[0] = dirp[-1];

			dirp[0] = copystr(dir);

		} while ((dir = sep) && (*--sep = ' ', TRUE));

		/* restore current working directory. */
		chdir(PWD);
	}

	if ((dir = getenv("OLDPWD")) == NULL)	/* bash(1) hack. */
		dir = HomeDir;
	set_str(&old_pwd, dir);
}

/*
 * Rotate the directory stack `offset' times (for pushd); or
 * Delete the `offset'th entry in the stack (for popd).
 * Entry number `offset' is relative to the current directory, i.e.,
 * the top of the stack, and may be positive or negative.
 * Returns whether some work has been done (it refuses to clobber current dir).
 */
private int	dirs_rotate __(( int _(offset), int _(delete_only) ));
private int
dirs_rotate(offset, delete_only)
{
	register int	num;

	if ((num = offset) < 0)
		num += DirSP + 1;

	if ((unsigned) num > DirSP)
		complain("%f: out of range; max of %d entries.", DirSP);

	if (num == 0)
		return NO;

	if (delete_only) {
		dirs_delete(DirSP - num);
	}
	else {
		if (True(DExtract)) {	/* Extract directory to top of stack. */
			char	extracteddir[FILESIZE];

			num = DirSP - num;
			/* Copy the extracted dirname, since the original is
			   destroyed by dirs_delete().  We have to delete _before_
			   DirSP is incremented and CWD is set since otherwise we
			   may run out of dirstack space (either this or we must
			   use dirs_grow). */
			strcpy(extracteddir, DirStack[num]);
			dirs_delete(num);
			DirSP++;
			setCWD(extracteddir);
		}
		else {			/* Rotate stack upwards `num' times. */
			do {
				register char	**dirp = &PWD;
				register char	*tmp = dirp[0];

				while (dirp > &DirStack[0]) {
					dirp[0] = dirp[-1];
					--dirp;
				}
				dirp[0] = tmp;
			} while (--num);
		}
		do_chdir(PWD);
	}
	return YES;
}

#else /* !CHDIR_EXT */

#   define dirs_grow()	complain("%f: full stack; max of %d pushes.", NDIRS-1)

#endif /* !CHDIR_EXT */

char *
pwd()
{
	return PWD;
}

#ifndef rel_name
/*
 * rel_name(fname) returns the pathname relative to current directory, if
 * possible.  The reason for this is that `fname' is usually an absolute
 * pathname, which can be slow when the pathname is long and there are
 * lots of symbolic links along the way (which has become very common in
 * my experience).  So, this speeds up access to files in the current
 * directory or its subdirectories.  It will not speed up things like
 * "../scm/foo.scm" simple because by the time we get here that's already
 * been expanded to an absolute pathname.  But this is a start.
 */
char *
rel_name(fname)
register const char	*fname;
{
	register char	*pwd;
	register int	n;

	if ((pwd = PWD) &&
	    (pwd[n = numcomp(fname, pwd)] == '\0') && (fname[n] == SLASH))
		fname += n + 1;

	return (char *) fname;
}
#endif

char *
pr_name(filename)
const char	*filename;
{
	static char		result[FILESIZE];
	register const char	*fname;

	if (fname = filename) {
		register char	*pwd = PWD;
		register int	n;

		if ((pwd[n = numcomp(fname, pwd)] == '\0') &&
		    (fname[n] == SLASH))
			fname += n + 1;
		else if ((n = HomeLen) > 1 && (numcomp(fname, HomeDir) == n)) {
			sprintf(result, "~%s", fname + n);
			fname = result;
		}
	}
	return (char *) fname;	/* return entire path name */
}

void
setCWD(d)
const char	*d;
{
	set_str(&PWD, d);
}

#ifndef TINY
extern int	ask_dir_only;
#endif

DEF_CMD( "cd", Cd, NO ) _IF(def CHDIR)
{
	char			dirbuf[FILESIZE];
	register const char	*dir;

#ifndef TINY
	ask_dir_only = YES;
#endif
	dir = ask_file((char *) 0, PWD, dirbuf);
	do_chdir(dir);
	setCWD(dir);
}

DEF_CMD( "dirs", prDIRS, NO ) _IF(def CHDIR)
{
	register char	**dirp = &PWD + 1;

#ifdef CHDIR_EXT
	if (exp_p) {
		register int	n = 0;

		TOstart("*Dirs*", YES);
		do {
			Typeout("%2d  %s", n++, pr_name(*--dirp));
		} while (dirp > &DirStack[0]);
		TOstop();
		return;
	}
#endif
	s_mess(ProcFmt);
	do {
		add_mess("%s ", pr_name(*--dirp));
	} while (dirp > &DirStack[0]);
}

DEF_CMD( "pwd", prCWD, NO ) _IF(def CHDIR)
{
	s_mess("%N: %f => %s", PWD);
}

/*
 * Pop top entry off directory stack, or swap top two entries.
 */
private void	popd_or_swapd __(( int _(swap) ));
private void
popd_or_swapd(swap)
{
	register char	**top;
	register char	*newdir, *olddir;

	if (DirSP == 0)
		complain("%f: no other directory.");

	top = &PWD;
	olddir = *top;
	*top = NULL;
	newdir = *--top;
	*top = olddir;		/* in case do_chdir() fails. */
	--DirSP;
	do_chdir(newdir);

	if (swap)
		++DirSP, *top++;
	else
		free(olddir);

	*top = newdir;
}

#define POPD()	popd_or_swapd(NO)
#define SWAPD()	popd_or_swapd(YES)

DEF_CMD( "pushd", Pushd, NO ) _IF(def CHDIR)
{
#ifdef CHDIR_EXT
	if (exp_p) {			/* equivalent to csh's "pushd +n" */
		if (dirs_rotate(exp, NO) == 0)
			SWAPD();
	}
	else {
#endif
		char		dirbuf[FILESIZE];
		register char	*newdir;

#ifndef TINY
		ask_dir_only = YES;
#endif
		newdir = ask_file((char *) 0, NullStr, dirbuf);

		if (*newdir == '\0') {	/* Wants to swap top two entries */
			SWAPD();
		}
		else {
#ifdef CHDIR_EXT
			register char	*base;
			register int	num;

			/* Quick hack to get "+n" or "-n" */
			if ((base = basename(newdir)) == pr_name(newdir) &&
			    (base[0] == '+' || base[0] == '-') &&
			    (num = chr_to_int(&base[1], 10, YES)) >= 0) {
				/* {{NOTE: chr_to_int("",...) returns 0!}} */
				if (base[1] == '\0')
					++num;
				if (base[0] == '-')
					num = -num;
				dirs_rotate(num, NO);
			}
			else {
				if (True(DUnique)) {
					while ((num = dirs_find(newdir)) >= 0)
						dirs_delete(num);
				}
#endif
				if (DirSP >= DirStkSize - 1)
					dirs_grow();
				do_chdir(newdir);
				++DirSP;
				setCWD(newdir);
#ifdef CHDIR_EXT
			}
#endif
		}
#ifdef CHDIR_EXT
	}
	exp_p = NO;
#endif
	prDIRS();
}

DEF_CMD( "popd", Popd, NO ) _IF(def CHDIR)
{
	if (DirSP == 0)
		complain("%f: directory stack is empty.");

#ifdef CHDIR_EXT			/* equivalent to csh's "popd +n" */
	if (!(exp_p && dirs_rotate(exp, YES)))
#endif
		POPD();
#ifdef CHDIR_EXT
	exp_p = NO;
#endif
	prDIRS();
}

char *
dfollow(file, into)
const char	*file;
char		*into;
{
	register char		*dp = into;
	register const char	*sp = file;
	register int		dots;
	register char		*ep = &dp[FILESIZE];
#if unix
#   define base into
#else
	register char		*base = dp;
#endif
#if vms /* first convert filename to UNIX syntax */
	char	*vms2ux __(( char *_(uxname), const char *_(vmsname) ));
	char	tmp[FILESIZE];

	if ((sp = vms2ux(tmp, sp)) == NULL)
		complain("\"%s\" [Bad file specification]", file);
#endif

	if (ISDIRSEP(*sp)) {			/* Absolute pathname */
#ifndef DOS
		*dp++ = *sp++;
#else
		if (PWD[1] == ':') {		/* Add current drive */
			*dp++ = *PWD;
			*dp++ = ':';
		}
		sp++, *dp++ = SLASH;
#endif
	}
	else {
#ifdef DOS
#   ifdef ATARIST
#	define isadevice(name) (strlen(name) == 4 && (name)[3] == ':')
#   else
		extern char	DevNames[];		/* from tune.c */
#	define isadevice(name) (CaseIgnore++,				\
				dots = LookingAt(DevNames, name, 0),	\
				CaseIgnore--,				\
				dots)
#   endif
		if (*sp && sp[1] == ':') { /* {GEM,MS}DOS explicit drive */
			*dp++ = *sp++;
			*dp++ = *sp++;
		}
		else if (isadevice(sp)) { /* {GEM,MS}DOS character device */
			strcpy(dp, sp);
			return into;
		}
		else	/* the appcpy statement */
#endif /* DOS */
#if vms		/* check for network node name */
		if (index(sp, '!') == index(sp, SLASH) - 1) {
			do *dp++ = *sp++; while (*sp != SLASH);
			base = dp;
		}
		else	/* the appcpy statement */
#endif /* vms */
		dp = appcpy(dp, PWD);	/* copy pwd and advance dest ptr. */
		if (dp[-1] != SLASH)
			*dp++ = SLASH;
	}
#ifdef DOS
	if (base[1] == ':')		/* skip drive name */
		base += 2;
#endif
	do {
		for (dots = 0; (*dp++ = *sp); ) {
			if (dp >= ep)	/* some healthy paranoia. */
				complain("Filename too long (max. %d): ...%s",
					 FILESIZE, sp);
			if (*sp == SLASH)
				break;
#if !(unix || vms)
			if (*sp == AltSlash)	{
				dp[-1] = SLASH;	/* normalize */
				break;
			}
#endif
			if (*sp++ == '.')
				dots++;
			else
				dots = 3;	/* fake: no "." or ".." */
		}
		if (dots < 3) {
			dp -= dots + 1;		/* adjust for "//", "/./" */
			if (dots == 2) {	/* and "/../" (parent)	  */
				/*
				 * we are here --------------------v
				 *			"[...]/foo/../bar"
				 * and want to end up here ----^
				 *			"/../baz"
				 * or here ---------------^
				 */
				--dp;
				while (dp > base && *--dp != SLASH) ;
				++dp;
			}
		}
	} while (*sp++);
	if (--dp > into && *dp == SLASH)
		*dp = '\0';
	FIX_FILENAME(into);
#undef base
#undef isadevice
	return into;
}

#undef DirStack
#undef DirStkSize

#else /* ! CHDIR */

#   define dfollow(file, into)	strcpy(into, file)

char *
pr_name(filename)
register const char	*filename;
{
	static char		result[FILESIZE];
	register const char	*fname;

	if (fname = filename) {
		if (HomeLen > 1 && numcomp(fname, HomeDir) == HomeLen) {
			sprintf(result, "~%s", fname + HomeLen);
			fname = result;
		}
	}
	return (char *) fname;
}

#endif /* CHDIR */

#ifndef cmp_ino
int
cmp_ino(file1, file2)
const char	*file1, *file2;
{
	struct stat	st1, st2;

	return (stat(file1, &st1) == 0 && stat(file2, &st2) == 0 &&
		ino_eq(st1.st_ino, st2.st_ino) && st1.st_dev == st2.st_dev);
}
#endif


#define UNKNOWNUSER(u) add_mess(" [unknown user: %s]", u), complain((char *) 0)

#ifdef PASSWD		/* if your /etc/passwd does not have standard layout,
			   or user names are maintained by NIS (Yellow Pages)
			   or something similar. */

#   include <pwd.h>

#   define GET_HOMEDIR(user, buf) do { \
		register struct passwd	*p = getpwnam(user);	\
		if (p == NULL)					\
			UNKNOWNUSER(user);			\
		strcpy(buf, p->pw_dir);				\
	} while (0)
#else /* !PASSWD */
#   if unix
#	define GET_HOMEDIR(user, buf) do { \
		register File	*fp;				\
		fp = open_file("/etc/passwd", iobuff,		\
			       F_READ|F_TEXT|F_COMPLAIN|F_QUIET);	\
		if (!re_fsearch(fp, sprint("^%s:[^:]*:[^:]*:[^:]*:[^:]*:\\([^:]*\\):", \
					   user)))		\
			UNKNOWNUSER(user);			\
		f_close(fp);					\
		putmatch(1, buf, FILESIZE);			\
	} while (0)
#   else /* !unix */
#	define GET_HOMEDIR(user, buf)	UNKNOWNUSER(user)
#   endif /* !unix */
#endif /* !PASSWD */

char *
PathParse(name, intobuf)
register const char	*name;
register char		*intobuf;
{
	char		localbuf[FILESIZE];
	register char	*lp = localbuf;

	intobuf[0] = lp[0] = '\0';
	if (*name == '\0')
		return intobuf;
	if (*name++ == '~') {
		if (ISDIRSEP(*name) || *name == '\0') {
			strcpy(lp, HomeDir);
		}
		else {
			/* "~user[/...]"; extract user name */
			do *lp++ = *name++; while (*name && !ISDIRSEP(*name));
			*lp = '\0';
			lp = localbuf;
			GET_HOMEDIR(lp, lp);
		}
	}
#ifdef CHDIR
#   ifdef CHDIR_EXT
	else if (name[-1] == '=') {	/* Directory stack access. */
		register int	n = 0,
				c,
				sign = 0;

		if (*name == '-') {
			++name;
			--sign;
		}
		while ((unsigned)(c = *name++ - '0') < 10)
			n = n * 10 + c;

		if (*--name == '\0' || ISDIRSEP(*name)) {
			if (n > DirSP) {
				add_mess((sign < 0) ? " [max =-%d]" :
					 " [max =%d]", DirSP);
				complain((char *)0);
			}
			if (sign < 0)
				n--;
			else
				n = DirSP - n;
			strcpy(lp, (n >= 0) ? DirStack[n] : old_pwd);
		}
		else {
			while (*--name != '=') ;	/* skip back. */
		}
	}
#   endif
#endif /* CHDIR */
#ifdef DOS
	else if (*--name == CTL('Q'))
		name++;
#else
	else if (*--name == '\\')
		name++;
#endif
	strcat(lp, name);
	return dfollow(lp, intobuf);
}

/*======================================================================
 * $Log: dirs.c,v $
 * Revision 14.31.0.12  1994/06/23  02:46:24  tom
 *  (prDIRS): fix name of scratch buffer.
 *
 * Revision 14.31.0.9  1994/01/29  04:27:01  tom
 * (dirs_rotate): fix serious bug in "dirs-extract" handling;
 * various comment fixes.
 *
 * Revision 14.31.0.6  1993/10/28  00:54:48  tom
 * dfollow: replace conditional strlwr() with FIX_FILENAME().
 *
 * Revision 14.31.0.1  1993/07/07  12:12:56  tom
 * (F_TEXT): new option for f_open et al.
 *
 * Revision 14.31  1993/02/16  11:44:19  tom
 * fix bug in dirs_find(); remove (void) casts.
 *
 * Revision 14.30  1993/01/26  18:43:08  tom
 * Initial revision, extracted from "io.c".
 * Added tcsh(1) features (as compile-time option CHDIR_EXT): numeric arguments
 * to pushd/popd to emulate "pushd/popd +N"; verbose "dirs" on numeric argument,
 * allow leading "=N" and "=-" in file names, initialize directory stack from
 * environment variables DIRS / OLDPWD; make chdir to old stack entries robust.
 *
 */
