/***************************************************************************
 * This program is Copyright (C) 1986, 1987, 1988 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"
#include "termcap.h"
#include "ctype.h"
#include "chars.h"
#include "disp.h"
#include "fp.h"
#include "scandir.h"
#include "screen.h"	/* for Placur */
#include "ask.h"
#include "delete.h"
#include "insert.h"
#include "extend.h"
#include "fmt.h"
#include "marks.h"
#include "move.h"

#include <signal.h>

#ifdef	MAC
# include "mac.h"
#else	/* !MAC */
# ifdef	STDARGS
#  include <stdarg.h>
# else
#  include <varargs.h>
# endif
# ifdef	F_COMPLETION
#  include <sys/stat.h>
# endif
#endif	/* !MAC */

int	AbortChar = CTL('G');
bool	DoEVexpand = YES;	/* should we expand evironment variables? */

bool	Asking = NO;
int	AskingWidth;

char	Minibuf[LBSIZE];
private Line	*CurAskPtr = NULL;	/* points at some line in mini-buffer */
private Buffer	*AskBuffer = NULL;	/* Askbuffer points to actual structure */

/* The way the mini-buffer works is this:  The first line of the mini-buffer
   is where the user does his stuff.  The rest of the buffer contains
   strings that the user often wants to use, for instance, file names, or
   common search strings, etc.  If he types C-N or C-P while in ask(), we
   bump the point up or down a line and extract the contents (we make sure
   is somewhere in the mini-buffer). */

private Buffer *
get_minibuf()
{
	if (AskBuffer) {		/* make sure ut still exists */
		register Buffer	*b;

		for (b = world; b != NULL; b = b->b_next)
			if (b == AskBuffer)
				return b;
	}
	AskBuffer = do_select((Window *)NULL, "*minibuf*");
	AskBuffer->b_type = B_SCRATCH;
	return AskBuffer;
}

/* Add a string to the mini-buffer. */

void
minib_add(str, movedown)
char	*str;
bool	movedown;
{
	register Buffer	*saveb = curbuf;

	SetBuf(get_minibuf());
	LineInsert(1);
	ins_str(str, NO);
	if (movedown)
		CurAskPtr = curline;
	SetBuf(saveb);
}

/* look for any substrings of the form $foo in linebuf, and expand
   them according to their value in the environment (if possible) -
   this munges all over curchar and linebuf without giving it a second
   thought (I must be getting lazy in my old age) */
private void
EVexpand()
{
	register int	c;
	register char	*lp = linebuf,
			*ep;
	char	varname[128],
		*vp,
		*lp_start;
	Mark	*m = MakeMark(curline, curchar, M_FLOATER);

	while ((c = *lp++) != '\0') {
		if (c == '$') {
			lp_start = lp - 1;	/* the $ */
			vp = varname;
			while ((c = *lp++) != '\0' && jisword(c))
				*vp++ = c;
			*vp = '\0';
			/* if we find an env. variable with the right
			   name, we insert it in linebuf, and then delete
			   the variable name that we're replacing - and
			   then we continue in case there are others ... */
			if ((ep = getenv(varname)) != NULL) {
				curchar = lp_start - linebuf;
				ins_str(ep, NO);
				del_char(FORWARD, (int)strlen(varname) + 1, NO);
				lp = linebuf + curchar;
			}
		}
	}
	ToMark(m);
	DelMark(m);
}

bool	InRealAsk = NO;

private char *
real_ask(delim, d_proc, def, prompt)
char	*delim,
	*def,
	*prompt;
bool	(*d_proc) proto((int));
{
	jmp_buf	savejmp;
	int	c,
		prompt_len;
	Buffer	*saveb = curbuf;
	volatile int	aborted = NO;
	int	no_typed = NO;
	data_obj	*push_cmd = LastCmd;
	int	saved_as, saved_ac;

#ifdef	MAC
	menus_off();
#endif
	if (InRealAsk)
		complain((char *) NULL);
	save_arg(saved_as, saved_ac);
	push_env(savejmp);
	InRealAsk = YES;
	SetBuf(get_minibuf());
	if (!inlist(AskBuffer->b_first, CurAskPtr))
		CurAskPtr = curline;
	prompt_len = strlen(prompt);
	ToFirst();	/* Beginning of buffer. */
	linebuf[0] = '\0';
	modify();
	makedirty(curline);

	if (setjmp(mainjmp)) {
		if (InJoverc) {		/* this is a kludge */
			aborted = YES;
			goto cleanup;
		}
	}

	for (;;) {
		clr_arg_value();
		last_cmd = this_cmd;
		init_strokes();
cont:
		s_mess("%s%s", prompt, linebuf);
		Asking = YES;
		AskingWidth = curchar + prompt_len;
		c = getch();

		if ((c == EOF) || (c != '\0' && strchr(delim, c) != NULL)) {
			if (d_proc == (bool(*) proto((int)))NULL || !(*d_proc)(c))
				break;
		} else if (c == AbortChar) {
			message("[Aborted]");
			aborted = YES;
			break;
		} else switch (c) {
		case CTL('N'):
		case CTL('P'):
			if (CurAskPtr != NULL) {
				int	n = (c == CTL('P') ? -arg_value() : arg_value());
				CurAskPtr = next_line(CurAskPtr, n);
				if (CurAskPtr == curbuf->b_first && CurAskPtr->l_next != NULL)
					CurAskPtr = CurAskPtr->l_next;
				(void) ltobuf(CurAskPtr, linebuf);
				modify();
				makedirty(curline);
				Eol();
				this_cmd = OTHER_CMD;
			}
			break;

		case CTL('R'):
			if (def)
				ins_str(def, NO);
			else
				rbell();
			break;

		default:
			dispatch(c);
			break;
		}
		if (curbuf != AskBuffer)
			SetBuf(AskBuffer);
		if (curline != curbuf->b_first) {
			CurAskPtr = curline;
			curline = curbuf->b_first;	/* with whatever is in linebuf */
		}
		if (this_cmd == ARG_CMD)
			goto cont;
	}
cleanup:
	pop_env(savejmp);

	LastCmd = push_cmd;
	restore_arg(saved_as, saved_ac);
	no_typed = (linebuf[0] == '\0');
	strcpy(Minibuf, linebuf);
	SetBuf(saveb);
	InRealAsk = Asking = Interactive = NO;
	if (!aborted) {
		if (!charp()) {
			Placur(ILI, 0);
			flushscreen();
		}
		if (no_typed)
			return NULL;
	} else
		complain(mesgbuf);
	return Minibuf;
}

#ifdef	STDARGS
	char *
ask(char *def, char *fmt, ...)
#else
	/*VARARGS2*/ char *
ask(def, fmt, va_alist)
	char	*def,
		*fmt;
	va_dcl
#endif
{
	char	prompt[128];
	char	*ans;
	va_list	ap;

	va_init(ap, fmt);
	format(prompt, sizeof prompt, fmt, ap);
	va_end(ap);
	ans = real_ask("\r\n", (bool (*) proto((int))) NULL, def, prompt);
	if (ans == NULL) {		/* Typed nothing. */
		if (def == NULL)
			complain("[No default]");
		ans = def;
	}
	return ans;
}

#ifdef	STDARGS
char *
do_ask(char *delim, bool (*d_proc) proto((int)), char *def, const char *fmt, ...)
#else
/*VARARGS4*/ char *
do_ask(delim, d_proc, def, fmt, va_alist)
	char	*delim,
		*def;
	const char	*fmt;
	bool	(*d_proc) proto((int));
	va_dcl
#endif
{
	char	prompt[128];
	va_list	ap;

	va_init(ap, fmt);
	format(prompt, sizeof prompt, fmt, ap);
	va_end(ap);
	return real_ask(delim, d_proc, def, prompt);
}

#ifdef	STDARGS
	bool
yes_or_no_p(char *fmt, ...)
#else
	/*VARARGS1*/ int
yes_or_no_p(fmt, va_alist)
	char	*fmt;
	va_dcl
#endif
{
	char	prompt[128];
	int	c;
	va_list	ap;

	va_init(ap, fmt);
	format(prompt, sizeof prompt, fmt, ap);
	va_end(ap);
	for (;;) {
		message(prompt);
		Asking = YES;	/* so redisplay works */
		AskingWidth = strlen(prompt);
		c = getch();
		Asking = NO;
		if (c == AbortChar)
			complain("[Aborted]");
		switch (CharUpcase(c)) {
		case 'Y':
			return YES;

		case 'N':
			return NO;

		default:
			add_mess("[Type Y or N]");
			SitFor(10);
		}
	}
	/* NOTREACHED */
}

#ifdef	F_COMPLETION

private char	*fc_filebase;
bool	DispBadFs = YES;	/* display bad file names? */
# ifndef	MSDOS
char	BadExtensions[128] = ".o";
# else	/* MSDOS */
char	BadExtensions[128] = ".obj .exe .com .bak .arc .lib .zoo";
# endif	/* MSDOS */

private int
bad_extension(name)
char	*name;
{
	char	*ip,
		*bads;
	size_t	namelen = strlen(name),
		ext_len;

#ifdef	UNIX
	if (strcmp(name, ".")==0 || strcmp(name, "..")==0)
		return YES;
#endif
	for (ip=bads=BadExtensions; *ip!='\0'; bads = ip+1) {
		if ((ip = strchr(bads, ' ')) == NULL)
			ip = bads + strlen(bads);
		ext_len = ip - bads;
		if (ext_len != 0 && ext_len < namelen
		&& strncmp(&name[namelen - ext_len], bads, ext_len) == 0)
			return YES;
	}
	return NO;
}

private int
f_match(file)
char	*file;
{
	int	len = strlen(fc_filebase);

	if (!DispBadFs && bad_extension(file))
		return NO;

	return len == 0
#ifdef	MSDOS
		|| casencmp(file, fc_filebase, strlen(fc_filebase)) == 0;
#else
		|| strncmp(file, fc_filebase, strlen(fc_filebase)) == 0;
#endif
}

private int
isdir(name)
char	*name;
{
	struct stat	stbuf;
	char	filebuf[FILESIZE];

	PathParse(name, filebuf);
	return stat(filebuf, &stbuf) != -1
		&& (stbuf.st_mode & S_IFDIR) == S_IFDIR;
}

private void
fill_in(dir_vec, n)
register char	**dir_vec;
int	n;
{
	int	minmatch = 0,
		numfound = 0,
		lastmatch = -1,
		i,
		the_same = YES, /* After filling in, are we the same
				    as when we were called? */
		is_ntdir;	/* Is Newly Typed Directory name */

	for (i = 0; i < n; i++) {
		/* if it's no, then we have already filtered them out
		   in f_match() so there's no point in doing it again */
		if (!DispBadFs || !bad_extension(dir_vec[i])) {
			minmatch = numfound == 0
				? strlen(dir_vec[i])
				: min(minmatch, numcomp(dir_vec[lastmatch], dir_vec[i]));
			lastmatch = i;
			numfound += 1;
		}
	}
	/* Ugh.  Beware--this is hard to get right in a reasonable
	   manner.  Please excuse this code--it's past my bedtime. */
	if (numfound == 0) {
		rbell();
	} else {
		Eol();
		if (minmatch > (int)strlen(fc_filebase)) {
			the_same = NO;
			null_ncpy(fc_filebase, dir_vec[lastmatch], (size_t) minmatch);
			Eol();
			makedirty(curline);
		}
		is_ntdir = numfound == 1 && curchar > 0
			&& linebuf[curchar - 1] != '/' && isdir(linebuf);
		if (the_same && !is_ntdir) {
			add_mess((n == 1) ? " [Unique]" : " [Ambiguous]");
			SitFor(7);
		}
		if (is_ntdir)
			insert_c('/', 1);
	}
}

/* called from do_ask() when one of "\r\n ?" is typed.  Does the right
   thing, depending on which. */

private bool
f_complete(c)
int	c;
{
	char
		dir[FILESIZE],
		**dir_vec;
	int
		nentries,
		i;

	if (DoEVexpand)
		EVexpand();
	if (c == CR || c == LF)
		return NO;	/* tells ask to return now */
	fc_filebase = strrchr(linebuf, '/');
#ifdef	MSDOS
	if (fc_filebase == NULL) {
		fc_filebase = strrchr(linebuf, '\\');
		if (fc_filebase == NULL)
			fc_filebase = strrchr(linebuf, ':');
	}
#endif	/* MSDOS */
	if (fc_filebase != NULL) {
		char	tmp[FILESIZE];

		fc_filebase += 1;
		null_ncpy(tmp, linebuf, (size_t) (fc_filebase - linebuf));
		if (tmp[0] == '\0')
			strcpy(tmp, "/");
		PathParse(tmp, dir);
	} else {
		fc_filebase = linebuf;
		strcpy(dir, ".");
	}
	if ((nentries = jscandir(dir, &dir_vec, f_match, alphacomp)) == -1) {
		add_mess(" [Unknown directory: %s]", dir);
		SitFor(7);
		return YES;
	}
	if (nentries == 0) {
		add_mess(" [No match]");
		SitFor(7);
	} else if (c == ' ' || c == '\t') {
		fill_in(dir_vec, nentries);
	} else {
		/* we're a '?' */
		int	maxlen = 0,
			ncols,
			col,
			lines,
			linespercol;

		TOstart("Completion", NO);	/* false means newline only on request */
		Typeout("(! means file will not be chosen unless typed explicitly)");
		Typeout((char *)NULL);
		Typeout("Possible completions (in %s):", dir);
		Typeout((char *)NULL);

		for (i = 0; i < nentries; i++)
			maxlen = max((int)strlen(dir_vec[i]), maxlen);
		maxlen += 4;	/* pad each column with at least 4 spaces */
		ncols = (CO - 2) / maxlen;
		linespercol = 1 + (nentries / ncols);

		for (lines = 0; lines < linespercol; lines++) {
			for (col = 0; col < ncols; col++) {
				bool	isbad;
				int	which;

				which = (col * linespercol) + lines;
				if (which >= nentries)
					break;
				isbad = DispBadFs && bad_extension(dir_vec[which]);
				Typeout("%s%-*s", isbad ? "!" : NullStr,
					maxlen - isbad, dir_vec[which]);
			}
			Typeout((char *) NULL);
		}
		TOstop();
	}
	freedir(&dir_vec, nentries);
	return YES;
}

#endif	/* F_COMPLETION */

char *
ask_file(prmt, def, buf)
const char	*prmt;
char
	*def,
	*buf;
{
	char	*ans,
		prompt[128],
		*pretty_name = pr_name(def, YES);

	if (prmt) {
		strcpy(prompt, prmt);
	} else {
		if (def != NULL && *def != '\0') {
			swritef(prompt, sizeof(prompt), ": %f (default %s) ",
				pretty_name);
			if ((int)strlen(prompt) * 2 >= CO)
				swritef(prompt, sizeof(prompt), ProcFmt);
		} else {
			swritef(prompt, sizeof(prompt), ProcFmt);
		}
	}
#ifdef	F_COMPLETION
	ans = real_ask("\r\n \t?", f_complete, pretty_name, prompt);
	if (ans == NULL && (ans = pretty_name) == NULL)
		complain("[No default file name]");
#else
	ans = ask(pretty_name, prompt);
#endif
	PathParse(ans, buf);

	return buf;
}
