/*	$OpenBSD: tags.c,v 1.17 2022/01/28 06:18:41 guenther Exp $	*/

/*
 * This file is in the public domain.
 *
 * Author: Sunil Nimmagadda <sunil@openbsd.org>
 */

#include <sys/queue.h>
#include <sys/stat.h>
#include <sys/tree.h>
#include <sys/types.h>
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#if defined(LIBBSD_OVERLAY) || defined(__FreeBSD__) || defined(__DragonFly__) || defined(__GLIBC__)
#  include <libutil.h>
#else
#include <util.h>
#endif

#include "def.h"

struct ctag;

static int               addctag(char *);
static int               atbow(void);
void                     closetags(void);
static int               ctagcmp(struct ctag *, struct ctag *);
static int               loadbuffer(char *);
static int               loadtags(const char *);
static int               pushtag(char *);
static int               searchpat(char *);
static struct ctag       *searchtag(char *);
static char              *strip(char *, size_t);
static void              unloadtags(void);

#define DEFAULTFN "tags"

char *tagsfn = NULL;
int  loaded  = FALSE;

/* ctags(1) entries are parsed and maintained in a tree. */
struct ctag {
	RB_ENTRY(ctag) entry;
	char *tag;
	char *fname;
	char *pat;
};
RB_HEAD(tagtree, ctag) tags = RB_INITIALIZER(&tags);
#ifdef __DragonFly__
RB_PROTOTYPE(tagtree, ctag, entry, ctagcmp);
#endif
RB_GENERATE(tagtree, ctag, entry, ctagcmp);

struct tagpos {
	SLIST_ENTRY(tagpos) entry;
	int    doto;
	int    dotline;
	char   *bname;
};
SLIST_HEAD(tagstack, tagpos) shead = SLIST_HEAD_INITIALIZER(shead);

int
ctagcmp(struct ctag *s, struct ctag *t)
{
	return strcmp(s->tag, t->tag);
}

/*
 * Record the filename that contain tags to be used while loading them
 * on first use. If a filename is already recorded, ask user to retain
 * already loaded tags (if any) and unload them if user chooses not to.
 */
/* ARGSUSED */
int
tagsvisit(int f, int n)
{
	char fname[NFILEN], *bufp, *temp;
	struct stat sb;
	
	if (getbufcwd(fname, sizeof(fname)) == FALSE)
		fname[0] = '\0';
	
	if (strlcat(fname, DEFAULTFN, sizeof(fname)) >= sizeof(fname)) {
		dobeep();
		ewprintf("Filename too long");
		return (FALSE);
	}
	
	bufp = eread("Visit tags table (default %s): ", fname,
	    NFILEN, EFFILE | EFCR | EFNEW | EFDEF, DEFAULTFN);
	if (bufp == NULL)
		return (ABORT);

	if (stat(bufp, &sb) == -1) {
		dobeep();
		ewprintf("stat: %s", strerror(errno));
		return (FALSE);
	} else if (S_ISREG(sb.st_mode) == 0) {
		dobeep();
		ewprintf("Not a regular file");
		return (FALSE);
	} else if (access(bufp, R_OK) == -1) {
		dobeep();
		ewprintf("Cannot access file %s", bufp);
		return (FALSE);
	}
	
	if (tagsfn == NULL) {
		if (bufp[0] == '\0') {
			if ((tagsfn = strdup(fname)) == NULL) {
				dobeep();
				ewprintf("Out of memory");
				return (FALSE);
			}
		} else {
			/* bufp points to local variable, so duplicate. */
			if ((tagsfn = strdup(bufp)) == NULL) {
				dobeep();
				ewprintf("Out of memory");
				return (FALSE);
			}
		}
	} else {
		if ((temp = strdup(bufp)) == NULL) {
			dobeep();
			ewprintf("Out of memory");
			return (FALSE);
		}
		free(tagsfn);
		tagsfn = temp;
		if (eyorn("Keep current list of tags table also") == FALSE) {
			ewprintf("Starting a new list of tags table");
			unloadtags();
		}
		loaded = FALSE;
	}
	return (TRUE);
}

/*
 * Ask user for a tag while treating word at dot as default. Visit tags
 * file if not yet done, load tags and jump to definition of the tag.
 */
int
findtag(int f, int n)
{
	char utok[MAX_TOKEN], dtok[MAX_TOKEN];
	char *tok, *bufp;
	int  ret;

	if (curtoken(f, n, dtok) == FALSE) {
		dtok[0] = '\0';
		bufp = eread("Find tag: ", utok, MAX_TOKEN, EFNUL | EFNEW);
	} else
		bufp = eread("Find tag (default %s): ", utok, MAX_TOKEN,
		    EFNUL | EFNEW, dtok);

	if (bufp == NULL)
		return (ABORT);
	else if	(bufp[0] == '\0')
		tok = dtok;
	else
		tok = utok;
	
	if (tok[0] == '\0') {
		dobeep();
		ewprintf("There is no default tag");
		return (FALSE);
	}
	
	if (tagsfn == NULL)
		if ((ret = tagsvisit(f, n)) != TRUE)
			return (ret);
	if (!loaded) {
		if (loadtags(tagsfn) == FALSE) {
			free(tagsfn);
			tagsfn = NULL;
			return (FALSE);
		}
		loaded = TRUE;
	}
	return pushtag(tok);
}

/*
 * Free tags tree.
 */
void
unloadtags(void)
{
	struct ctag *var, *nxt;
	
	for (var = RB_MIN(tagtree, &tags); var != NULL; var = nxt) {
		nxt = RB_NEXT(tagtree, &tags, var);
		RB_REMOVE(tagtree, &tags, var);
		/* line parsed with fparseln needs to be freed */
		free(var->tag);
		free(var);
	}
}

/*
 * Lookup tag passed in tree and if found, push current location and 
 * buffername onto stack, load the file with tag definition into a new
 * buffer and position dot at the pattern.
 */
/*ARGSUSED */
int
pushtag(char *tok)
{
	struct ctag *res;
	struct tagpos *s;
	char bname[NFILEN];
	int doto, dotline;
	
	if ((res = searchtag(tok)) == NULL)
		return (FALSE);
		
	doto = curwp->w_doto;
	dotline = curwp->w_dotline;
	/* record absolute filenames. Fixes issues when mg's cwd is not the
	 * same as buffer's directory.
	 */
	if (strlcpy(bname, curbp->b_cwd, sizeof(bname)) >= sizeof(bname)) {
		dobeep();
		ewprintf("filename too long");
		return (FALSE);
	}
	if (strlcat(bname, curbp->b_bname, sizeof(bname)) >= sizeof(bname)) {
		dobeep();
		ewprintf("filename too long");
		return (FALSE);
	}	

	if (loadbuffer(res->fname) == FALSE)
		return (FALSE);
	
	if (searchpat(res->pat) == TRUE) {
		if ((s = malloc(sizeof(struct tagpos))) == NULL) {
			dobeep();
			ewprintf("Out of memory");
			return (FALSE);
		}
		if ((s->bname = strdup(bname)) == NULL) {
			dobeep();
			ewprintf("Out of memory");
			free(s);
			return (FALSE);
		}
		s->doto = doto;
		s->dotline = dotline;
		SLIST_INSERT_HEAD(&shead, s, entry);
		return (TRUE);
	} else {
		dobeep();
		ewprintf("%s: pattern not found", res->tag);
		return (FALSE);
	}
	/* NOTREACHED */
	return (FALSE);
}

/*
 * If tag stack is not empty pop stack and jump to recorded buffer, dot.
 */
/* ARGSUSED */
int
poptag(int f, int n)
{
	struct line *dotp;
	struct tagpos *s;
	
	if (SLIST_EMPTY(&shead)) {
		dobeep();
		ewprintf("No previous location for find-tag invocation");
		return (FALSE);
	}
	s = SLIST_FIRST(&shead);
	SLIST_REMOVE_HEAD(&shead, entry);
	if (loadbuffer(s->bname) == FALSE)
		return (FALSE);
	curwp->w_dotline = s->dotline;
	curwp->w_doto = s->doto;
	
	/* storing of dotp in tagpos wouldn't work out in cases when
	 * that buffer is killed by user(dangling pointer). Explicitly
	 * traverse till dotline for correct handling. 
	 */
	dotp = curwp->w_bufp->b_headp;
	while (s->dotline--)
		dotp = dotp->l_fp;
	
	curwp->w_dotp = dotp;
	free(s->bname);
	free(s);
	return (TRUE);
}

/*
 * Parse the tags file and construct the tags tree. Remove escape 
 * characters while parsing the file.
 */
int
loadtags(const char *fn)
{
	char *l;
	FILE *fd;
	
	if ((fd = fopen(fn, "r")) == NULL) {
		dobeep();
		ewprintf("Unable to open tags file: %s", fn);
		return (FALSE);
	}
	while ((l = fparseln(fd, NULL, NULL, "\\\\\0",
	    FPARSELN_UNESCCONT | FPARSELN_UNESCREST)) != NULL) {
		if (addctag(l) == FALSE) {
			fclose(fd);
			return (FALSE);
		}
	}
	fclose(fd);
	return (TRUE);
}

/*
 * Cleanup and destroy tree and stack.
 */
void
closetags(void)
{
	struct tagpos *s;	
	
	while (!SLIST_EMPTY(&shead)) {
		s = SLIST_FIRST(&shead);
		SLIST_REMOVE_HEAD(&shead, entry);
		free(s->bname);
		free(s);
	}
	unloadtags();
	free(tagsfn);
}

/*
 * Strip away any special characters in pattern.
 * The pattern in ctags isn't a true regular expression. Its of the form
 * /^xxx$/ or ?^xxx$? and in some cases the "$" would be missing. Strip 
 * the leading and trailing special characters so the pattern matching
 * would be a simple string compare. Escape character is taken care by 
 * fparseln.
 */
char *
strip(char *s, size_t len)
{
	/* first strip trailing special chars */	
	s[len - 1] = '\0';
	if (s[len - 2] == '$')
		s[len - 2] = '\0';
	
	/* then strip leading special chars */
	s++;
	if (*s == '^')
		s++;
	
	return s;
}

/*
 * tags line is of the format "<tag>\t<filename>\t<pattern>". Split them
 * by replacing '\t' with '\0'. This wouldn't alter the size of malloc'ed
 * l, and can be freed during cleanup.
 */
int
addctag(char *l)
{
	struct ctag *t;
	
	if ((t = malloc(sizeof(struct ctag))) == NULL) {
		dobeep();
		ewprintf("Out of memory");
		return (FALSE);
	}
	t->tag = l;
	if ((l = strchr(l, '\t')) == NULL)
		goto cleanup;
	*l++ = '\0';
	t->fname = l;
	if ((l = strchr(l, '\t')) == NULL)
		goto cleanup;
	*l++ = '\0';
	if (*l == '\0')
		goto cleanup;
	t->pat = strip(l, strlen(l));
	RB_INSERT(tagtree, &tags, t);
	return (TRUE);
cleanup:
	free(t);
	free(l);
	return (FALSE);
}

/*
 * Search through each line of buffer for pattern.
 */
int
searchpat(char *s_pat)
{
	struct line *lp;
	int dotline;
	size_t plen;

	plen = strlen(s_pat);
	dotline = 1;
	lp = lforw(curbp->b_headp);
	while (lp != curbp->b_headp) {
		if (ltext(lp) != NULL && plen <= llength(lp) &&
		    (strncmp(s_pat, ltext(lp), plen) == 0)) {
			curwp->w_doto = 0;
			curwp->w_dotp = lp;
			curwp->w_dotline = dotline;
			return (TRUE);
		} else {
			lp = lforw(lp);
			dotline++;
		}
	}
	return (FALSE);
}

/*
 * Return TRUE if dot is at beginning of a word or at beginning 
 * of line, else FALSE.
 */
int
atbow(void)
{
	if (curwp->w_doto == 0)
		return (TRUE);
	if (ISWORD(curwp->w_dotp->l_text[curwp->w_doto]) &&
	    !ISWORD(curwp->w_dotp->l_text[curwp->w_doto - 1]))
	    	return (TRUE);
	return (FALSE);
}

/*
 * Extract the word at dot without changing dot position.
 */
int
curtoken(int f, int n, char *token)
{
	struct line *odotp;
	int odoto, tdoto, odotline, size, r;
	char c;
	
	/* Underscore character is to be treated as "inword" while
	 * processing tokens unlike mg's default word traversal. Save
	 * and restore its cinfo value so that tag matching works for
	 * identifier with underscore.
	 */
	c = cinfo['_'];
	cinfo['_'] = _MG_W;
	
	odotp = curwp->w_dotp;
	odoto = curwp->w_doto;
	odotline = curwp->w_dotline;
	
	/* Move backword unless we are at the beginning of a word or at
	 * beginning of line.
	 */
	if (!atbow())
		if ((r = backword(f, n)) == FALSE)
			goto cleanup;
		
	tdoto = curwp->w_doto;

	if ((r = forwword(f, n)) == FALSE)
		goto cleanup;
	
	/* strip away leading whitespace if any like emacs. */
	while (ltext(curwp->w_dotp) && 
	    isspace(lgetc(curwp->w_dotp, tdoto)))
		tdoto++;

	size = curwp->w_doto - tdoto;
	if (size <= 0 || size >= MAX_TOKEN || 
	    ltext(curwp->w_dotp) == NULL) {
		r = FALSE;
		goto cleanup;
	}    
	strncpy(token, ltext(curwp->w_dotp) + tdoto, size);
	token[size] = '\0';
	r = TRUE;
	
cleanup:
	cinfo['_'] = c;
	curwp->w_dotp = odotp;
	curwp->w_doto = odoto;
	curwp->w_dotline = odotline;
	return (r);
}

/*
 * Search tagstree for a given token.
 */
struct ctag *
searchtag(char *tok)
{
	struct ctag t, *res;

	t.tag = tok;
	if ((res = RB_FIND(tagtree, &tags, &t)) == NULL) {
		dobeep();
		ewprintf("No tag containing %s", tok);
		return (NULL);
	}
	return res;
}

/*
 * This is equivalent to filevisit from file.c.
 * Look around to see if we can find the file in another buffer; if we
 * can't find it, create a new buffer, read in the text, and switch to
 * the new buffer. *scratch*, *grep*, *compile* needs to be handled 
 * differently from other buffers which have "filenames".
 */
int
loadbuffer(char *bname)
{
	struct buffer *bufp;
	char *adjf;

	/* check for special buffers which begin with '*' */
	if (bname[0] == '*') {
		if ((bufp = bfind(bname, FALSE)) != NULL) {
			curbp = bufp;
			return (showbuffer(bufp, curwp, WFFULL));
		} else {
			return (FALSE);
		}
	} else {	
		if ((adjf = adjustname(bname, TRUE)) == NULL)
			return (FALSE);
		if ((bufp = findbuffer(adjf)) == NULL)
			return (FALSE);
	}
	curbp = bufp;
	if (showbuffer(bufp, curwp, WFFULL) != TRUE)
		return (FALSE);
	if (bufp->b_fname[0] == '\0') {
		if (readin(adjf) != TRUE) {
			killbuffer(bufp);
			return (FALSE);
		}
	}
	return (TRUE);
}
