/*
 * command history
 *
 * only implements in-memory history.
 */

#ifndef lint
static char *RCSid = "$Id: history.c,v 1.7 93/06/01 23:40:36 sjg Exp $";
#endif
/*
 *	This file contains
 *	a)	the original in-memory history  mechanism
 *	b)	a simple file saving history mechanism done by  sjg@zen
 *		define EASY_HISTORY to get this
 *	c)	a more complicated mechanism done by  pc@hillside.co.uk
 *		that more closely follows the real ksh way of doing
 *		things. You need to have the mmap system call for this
 *		to work on your system
 */

#include "config.h"
#include "stdh.h"

#ifdef EASY_HISTORY

#include <errno.h>
#include <setjmp.h>
#include "sh.h"

static FILE *hist_fh = NULL;
static FILE *hist_open ARGS((char *mode));
#ifndef HISTFILE
# define HISTFILE ".pdksh_hist"
#endif

#else
/*	Defines and includes for the complicated case */

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <sys/mman.h>
#include <errno.h>
#include <setjmp.h>
#include "sh.h"

/*
 *	variables for handling the data file
 */
static char	*hname;
static int	histfd;
static int	hsize;
static int	hstarted;

static int hist_count_lines ARGS((unsigned char *, int));
static int hist_shrink ARGS((unsigned char *, int));
static unsigned char *hist_skip_back ARGS((unsigned char *,int *,int));
static void histload ARGS((Source *, unsigned char *, int));
static void histinsert ARGS((Source *, int, unsigned char *));
static void writehistfile ARGS((int, char *));
static int sprinkle ARGS((int));

#ifdef MAP_FILE
#define	MAP_FLAGS	MAP_FILE|MAP_PRIVATE
#else
#define	MAP_FLAGS	MAP_PRIVATE
#endif

#endif	/* of EASY_HISTORY */

  
char   *histrpl();
char  **current;
int	curpos;

c_fc(wp)
	register char **wp;
{
	register char *id;
	FILE *f;
	struct temp *tf;
	register char **hp;
	char **hbeg, **hend;
	char *p, *cmd = NULL;
	int lflag = 0, nflag = 0, sflag = 0, rflag = 0, gflag = 0;
	int done = 0;
	void histbackup();

	for (wp++; (id = *wp) != NULL && *id++ == '-' && !done; wp++)
		while (*id && !done) {
			switch (*id++) {
			  case 'l':
				lflag++;
				break;
			  case 'n':
				nflag++;
				break;
			  case 'r':
				rflag++;
				break;
			  case 'g':
				gflag++;
				break;
			  case 'e':
				if (++wp && (p = *wp)) {
					if (p[0] == '-' && !p[1]) {
						sflag++;
					} else {
						cmd = alloc((size_t)(strlen(p)+4),ATEMP);
						strcpy(cmd, p);
						strcat(cmd, " $_");
					}
				} else
					errorf("argument expected\n");
				id = "";
				break;
			  default:
				wp--;
				done++;
				break;
			}
		}

	if (sflag) {
		char *pat = NULL, *rep = NULL;

		hp = histptr - 1;
		while ((id = *wp++) != NULL) {
			/* todo: multiple substitutions */
			if ((p = strchr(id, '=')) != NULL) {
				pat = id;
				rep = p;
				*rep++ = '\0';
			} else
				hp = histget(id);
		}

		if (hp == NULL || hp < history)
			errorf("cannot find history\n");
		if (pat == NULL)
			strcpy(line, *hp);
		else
			histrpl(*hp, pat, rep, gflag);
		histbackup();
#ifdef EASY_HISTORY
		histsave(line); 
#else
		histsave(source->line+1, line, 1);
#endif
		histpush--; 
		line[0] = '\0';
		return 0;
	}

	if (*wp != NULL) {
		hbeg = histget(*wp++); /* first */
		if (*wp != NULL)
			hend = histget(*wp++); /* last */
		else if (lflag)
			hend = histptr;
		else
			hend = hbeg;
	} else {
		if (lflag)
			hbeg = histptr - 16, hend = histptr;
		else
			hbeg = hend = histptr - 1;
		if (hbeg < history)
			hbeg = history;
	}
	if (hbeg == NULL || hend == NULL)
		errorf("can't find history\n");

	if (lflag)
		f = stdout;
	else {
		nflag++;
		tf = maketemp(ATEMP);
		tf->next = e.temps; e.temps = tf;
		f = fopen(tf->name, "w");
		if (f == NULL)
			errorf("cannot create temp file %s", tf->name);
		setvbuf(f, (char *)NULL, _IOFBF, BUFSIZ);
	}

	for (hp = (rflag ? hend : hbeg); rflag ? (hp >= hbeg) : (hp <= hend);
	      rflag ? hp-- : hp++) {
		if (!nflag)
			fprintf(f, "%3d: ", source->line - (int)(histptr-hp));
		fprintf(f, "%s\n", *hp);
	}

	if (lflag)
		return 0;
	else
		fclose(f);

	setstr(local("_"), tf->name);
	if (cmd) {
		command(cmd); /* edit temp file */
		afree(cmd, ATEMP);
	} else
		command("${FCEDIT:-/bin/ed} $_");

	f = fopen(tf->name, "r");
	if (f == NULL)
		errorf("cannot open temp file %s\n", tf->name);
	setvbuf(f, (char *)NULL, _IOFBF, BUFSIZ);
	/* we push the editted lines onto the history list */
	while (fgets(line, sizeof(line), f) != NULL) {
#ifdef EASY_HISTORY
		histsave(line); 
#else
		histsave(source->line, line, 1); 
#endif
		histpush--; 
	}
	line[0] = '\0';
	fclose(f);

	return 0;
}

/******************************/
/* Back up over last histsave */
/******************************/
void
histbackup()
{
	static int last_line = -1;

	if (histptr > history && last_line != source->line) { 
		source->line--;
		afree((void*)*histptr, APERM);
		histptr--;
		last_line = source->line;
	}
}

/*
 * get pointer to history given pattern
 * pattern is a number or string
 */
char **
histget(str)
	char *str;
{
	register char **hp = NULL;

	if (*str == '-')
		hp = histptr + getn(str);
	else
	if (digit(*str))
		hp = histptr + (getn(str) - source->line);
	else 
	if (*str == '?') {	/* unanchored match */
		for (hp = histptr-1; hp >= history; hp--)
			if (strstr(*hp, str+1) != NULL)
				break;
	} else {		/* anchored match */
		for (hp = histptr; hp >= history; hp--)
			if (strncmp(*hp, str, strlen(str)) == 0)
				break;
	}

	return (history <= hp && hp <= histptr) ? hp : NULL;
}

char *
histrpl(s, pat, rep, global)
	char *s;
	char *pat, *rep;
	int global;
{
	char *s1, *p, *last = NULL;
	int len = strlen(pat);

	if (strlen(s) - strlen(pat) + strlen(rep) >= LINE)
		errorf("substitution too long\n");
	line[0] = '\0';
	p = line;
	while (s1 = strstr(s, pat)) {
		strncpy(p, s, s1 - s);		/* first part */
		strcpy(p + (s1 - s), rep);	/* replacement */
		s = s1 + len;
		last = s1;
		p = strchr(p, 0);
		if (!global)
			s = "";
	}
	if (last)
		strcpy(p, last + len);		/* last part */
	else
		errorf("substitution failed\n");
	return line;
}

/*
 * Return the current position.
 */
char **
histpos()
{
	return current;
}

int
histN()
{
	return curpos;
}

int
histnum(n)
{
	int	last = histptr - history;

	if (n < 0 || n >= last) {
		current = histptr;
		curpos = last;
		return last;
	}  else {
		current = &history[n];
		curpos = n;
		return n;
	}
}

/*
 * This will become unecessary if histget is modified to allow
 * searching from positions other than the end, and in either 
 * direction.
 */
char *
findhist(start, fwd, str)
	int	start;
	int	fwd;
	char 	*str;
{
	int	 pos = start;
	char	 *line, *last;

	/* XXX check that we are valid after this */
	if (fwd)
		pos++;
	else
		pos--;
	histnum(pos);
	line = *histpos();
	do {
		last = line;
		if (strstr(line, str) != 0) {
			/* keep position current */
			return (line);
		}
		if (fwd)
			pos++;
		else
			pos--;
		histnum(pos);
		line = *histpos();
	} while (line && *line && line != last && pos>0);

	histnum(start);
	if (pos <= 0)
		return (char*)-1; /* TODO */
	return NULL;
}

#ifdef EASY_HISTORY
/*
 * save command in history
 */
void
histsave(cmd)
	char *cmd;
{
	register char **hp = histptr;
	char *cp;

	if (++hp >= history + HISTORY) { /* remove oldest command */
		afree((void*)*history, APERM);
		for (hp = history; hp < history + HISTORY - 1; hp++)
			hp[0] = hp[1];
	}
	*hp = strsave(cmd, APERM);
	if ((cp = strchr(*hp, '\n')) != NULL)
		*cp = '\0';
	histptr = hp;
}

/*
 * 92-04-25 <sjg@zen>
 * A simple history file implementation.
 * At present we only save the history when we exit.
 * This can cause problems when there are multiple shells are 
 * running under the same user-id.  The last shell to exit gets 
 * to save its history.
 */
void
hist_init(s)
  Source *s;
{
  static int once = 0;
  FILE *fh;
  
  if (once++)
    return;

  if (fh = hist_open("r"))
  {
    while (fgets(line, sizeof(line), fh) != NULL)
    {
      histsave(line); 
      s->line++;
    }
    line[0] = '\0';
    fclose(fh);
#if 0	/* this might be a good idea? */
    hist_fh = hist_open("a");
#endif
  }
  
}

void
init_histvec()
{	;	}

/*
 * save our history.
 * We check that we do not have more than we are allowed.
 * If the history file is read-only we do nothing.
 * Handy for having all shells start with a useful history set.
 */

void
hist_finish()
{
  static int once = 0;
  FILE *fh;
  register int i, mx;
  register char **hp, *mode = "w";
  
  if (once++)
    return;
  if ((mx = atoi(strval(global("HISTSIZE")))) > HISTORY || mx <= 0)
    mx = HISTORY;
  /* check how many we have */
  i = histptr - history;
  if (i >= mx)
  {
    hp = &histptr[-mx];
  }
  else
  {
    hp = history;
  }
  if (fh = hist_open(mode))
  {
    for (i = 0; i < mx && hp[i]; i++)
      fprintf(fh, "%s\n", hp[i]);
    fclose(fh);
  }
}


/*
 * simply grab the nominated history file.
 */
static FILE *
hist_open(mode)
  char *mode;
{
  register char *rcp;
  FILE *fh;
  char name[128];
  
  if ((rcp = strval(global("HISTFILE"))) == NULL || *rcp == '\0')
  {
    (void) sprintf(name, "%s/%s", strval(global("HOME")), HISTFILE);
    rcp = name;
  }
  return fopen(rcp, mode);
}

#else /* EASY_HISTORY */

/*
 *	Routines added by Peter Collinson BSDI(Europe)/Hillside Systems to
 *	a) permit HISTSIZE to control number of lines of history stored
 *	b) maintain a physical history file
 *
 *	It turns out that there is a lot of ghastly hackery here
 */


/*
 * save command in history
 */
void
histsave(lno, cmd, dowrite)
	int lno;
	char *cmd;
	int dowrite;
{
	register char **hp;
	char *cp;

	cmd = strsave(cmd, APERM);
	if ((cp = strchr(cmd, '\n')) != NULL)
		*cp = '\0';

	if (histfd && dowrite)
		writehistfile(lno, cmd);

	hp = histptr;
		
	if (++hp >= history + histsize) { /* remove oldest command */
		afree((void*)*history, APERM);
		for (hp = history; hp < history + histsize - 1; hp++)
			hp[0] = hp[1];
	}
	*hp = cmd;
	histptr = hp;
}

/*
 *	set history
 *	this means reallocating the dataspace
 */
void
sethistsize(n)
	int n;
{
	int	offset;
	
	if (n != histsize) {
		offset = histptr - history;
		history = (char **)aresize(history, n*sizeof(char *), APERM);

		if (n < histsize && offset > histsize)
			offset = histsize;

		histsize = n;
		histptr = history + offset;
	}
}

/*
 *	set history file
 *	This can mean reloading/resetting/starting history file
 *	maintenance
 */
void
sethistfile(name)
	char *name;
{
	/* if not started then nothing to do */
	if (hstarted == 0)
		return;

	/* if the name is the same as the name we have */
	if (hname && strcmp(hname, name) == 0)
		return;

	/*
	 * its a new name - possibly
	 */
	if (histfd) {
		/* yes the file is open */
		(void) close(histfd);
		histfd = 0;
		hsize = 0;
		afree(hname, APERM);
		hname = NULL;
		/* let's reset the history */
		histptr = history - 1;
		source->line = 0;
	}

	hist_init(source);
}

/*
 *	initialise the history vector
 */
void
init_histvec()
{
	if (history == (char **)NULL) {
		history = (char **)alloc(histsize*sizeof (char *), APERM);
		histptr = history-1;
	}
}
	
/*
 *	Write history data to a file nominated by HISTFILE
 *	if HISTFILE is unset then history still happens, but
 *	the data is not written to a file
 *	All copies of ksh looking at the file will maintain the
 *	same history. This is ksh behaviour.
 *
 *	This stuff uses mmap()
 *	if your system ain't got it - then you'll have to undef HISTORYFILE
 */
	
/*
 *	Open a history file
 *	Format is:
 *	Bytes 1, 2: HMAGIC - just to check that we are dealing with
 *		    the correct object
 *	Then follows a number of stored commands
 *	Each command is
 *	<command byte><command number(4 bytes)><bytes><null>
 */
#define HMAGIC1		0xab
#define HMAGIC2		0xcd
#define COMMAND		0xff

void
hist_init(s)
	Source *s;
{
	unsigned char	*base;
	int	lines;
	int	bytes;
	int	fd;
	
	hstarted = 1;
	
	if (flag[FTALKING] == 0)
		return;

	hname = strval(global("HISTFILE"));
	if (hname == NULL)
		return;
	hname = strsave(hname, APERM);

  retry:
	/* we have a file and are interactive */
	if ((fd = open(hname, O_RDWR|O_CREAT|O_APPEND, 0600)) < 0)
		return;

	histfd = fcntl(fd, F_DUPFD, FDBASE);
	close(fd);
	
	(void) fd_clexec(histfd);

	(void) flock(histfd, LOCK_EX);

	hsize = lseek(histfd, 0L, L_XTND);

	if (hsize == 0) {
		/* add magic */
		if (sprinkle(histfd)) {
			hist_finish();
			return;
		}
	}
	else if (hsize > 0) {
		/*
		 * we have some data
		 */
		base = (unsigned char *)mmap(0, hsize, PROT_READ, MAP_FLAGS, histfd, 0);
		/*
		 * check on its validity
		 */
		if ((int)base == -1 || *base != HMAGIC1 || base[1] != HMAGIC2) {
			if ((int)base !=  -1)
				munmap((caddr_t)base, hsize);
			hist_finish();
			unlink(hname);
			goto retry;
		}
		if (hsize > 2) {
			lines = hist_count_lines(base+2, hsize-2);
			if (lines > histsize) {
				/* we need to make the file smaller */
				if (hist_shrink(base, hsize))
					unlink(hname);
				munmap((caddr_t)base, hsize);
				hist_finish();
				goto retry;
			}
		}
		histload(s, base+2, hsize-2);
		munmap((caddr_t)base, hsize);
	}
	(void) flock(histfd, LOCK_UN);
	hsize = lseek(histfd, 0L, L_XTND);
}

typedef enum state {
	shdr,		/* expecting a header */
	sline,		/* looking for a null byte to end the line */
	sn1,		/* bytes 1 to 4 of a line no */
	sn2, sn3, sn4,
} State;

static int
hist_count_lines(base, bytes)
	register unsigned char *base;
	register int bytes;
{
	State state = shdr;
	register lines = 0;
	
	while (bytes--) {
		switch (state)
		{
		case shdr:
			if (*base == COMMAND)
				state = sn1;
			break;
		case sn1:
			state = sn2; break;
		case sn2:
			state = sn3; break;
		case sn3:
			state = sn4; break;
		case sn4:
			state = sline; break;
		case sline:
			if (*base == '\0')
				lines++, state = shdr;
		}
		base++;
	}
	return lines;
}

/*
 *	Shrink the history file to histsize lines
 */
static int
hist_shrink(oldbase, oldbytes)
	unsigned char *oldbase;
	int oldbytes;
{
	int fd;
	char	nfile[1024];
	struct	stat statb;
	unsigned char *nbase = oldbase;
	int nbytes = oldbytes;

	nbase = hist_skip_back(nbase, &nbytes, histsize);
	if (nbase == NULL)
		return 1;
	if (nbase == oldbase)
		return 0;
	
	/*
	 *	create temp file
	 */
	(void) sprintf(nfile, "%s.%d", hname, getpid());
	if ((fd = creat(nfile, 0600)) < 0)
		return 1;

	if (sprinkle(fd)) {
		close(fd);
		unlink(nfile);
		return 1;
	}
	if (write(fd, nbase, nbytes) != nbytes) {
		close(fd);
		unlink(nfile);
		return 1;
	}
	/*
	 *	worry about who owns this file
	 */
	if (fstat(histfd, &statb) >= 0)
		fchown(fd, statb.st_uid, statb.st_gid);
	close(fd);
	
	/*
	 *	rename
	 */
	if (rename(nfile, hname) < 0)
		return 1;
	return 0;
}
	

/*
 *	find a pointer to the data `no' back from the end of the file
 *	return the pointer and the number of bytes left
 */
static unsigned char *
hist_skip_back(base, bytes, no)
	unsigned char *base;
	int *bytes;
	int no;
{
	register int lines = 0;
	register unsigned char *ep;

	

	for (ep = base + *bytes; ep > base; ep--)
	{
		while (*ep != COMMAND) {
			if (--ep == base)
				break;
		}
		if (++lines == no) {
			*bytes = *bytes - ((char *)ep - (char *)base);
			return ep;
		}
	}
	if (ep > base)
		return base;
	return NULL;
}

/*
 *	load the history structure from the stored data
 */
static void
histload(s, base, bytes)
	Source *s;
	register unsigned char *base;
	register int bytes;
{
	State state;
	int	lno;
	unsigned char	*line;
	
	for (state = shdr; bytes-- > 0; base++) {
		switch (state) {
		case shdr:
			if (*base == COMMAND)
				state = sn1;
			break;
		case sn1:
			lno = (((*base)&0xff)<<24);
			state = sn2;
			break;
		case sn2:
			lno |= (((*base)&0xff)<<16);
			state = sn3;
			break;
		case sn3:
			lno |= (((*base)&0xff)<<8);
			state = sn4;
			break;
		case sn4:
			lno |= (*base)&0xff;
			line = base+1;
			state = sline;
			break;
		case sline:
			if (*base == '\0') {
				/* worry about line numbers */
				if (histptr >= history && lno-1 != s->line) {
					/* a replacement ? */
					histinsert(s, lno, line);
				}
				else {
					s->line = lno;
					histsave(lno, (char *)line, 0);
				}
				state = shdr;
			}
		}
	}
}
				
/*
 *	Insert a line into the history at a specified number
 */
static void
histinsert(s, lno, line)
	Source *s;
	int lno;
	unsigned char *line;
{
	register char **hp;
	
	if (lno >= s->line-(histptr-history) && lno <= s->line) {
		hp = &histptr[lno-s->line];
		if (*hp)
			afree((void*)*hp, APERM);
		*hp = strsave((char *)line, APERM);
	}
}

/*
 *	write a command to the end of the history file
 *	This *MAY* seem easy but it's also necessary to check
 *	that the history file has not changed in size.
 *	If it has - then some other shell has written to it
 *	and we should read those commands to update our history
 */
static void
writehistfile(lno, cmd)
	int lno;
	char *cmd;
{
	int	sizenow;
	unsigned char	*base;
	unsigned char	*new;
	int	bytes;
	char	hdr[5];
	
	(void) flock(histfd, LOCK_EX);
	sizenow = lseek(histfd, 0L, L_XTND);
	if (sizenow != hsize) {
		/*
		 *	Things have changed
		 */
		if (sizenow > hsize) {
			/* someone has added some lines */
			bytes = sizenow - hsize;
			base = (unsigned char *)mmap(0, sizenow, PROT_READ, MAP_FLAGS, histfd, 0);
			if ((int)base == -1)
				goto bad;
			new = base + hsize;
			if (*new != COMMAND) {
				munmap((caddr_t)base, sizenow);
				goto bad;
			}
			source->line--;
			histload(source, new, bytes);
			source->line++;
			lno = source->line;
			munmap((caddr_t)base, sizenow);
			hsize = sizenow;
		} else {
			/* it has shrunk */
			/* but to what? */
			/* we'll give up for now */
			goto bad;
		}
	}
	/*
	 *	we can write our bit now
	 */
	hdr[0] = COMMAND;
	hdr[1] = (lno>>24)&0xff;
	hdr[2] = (lno>>16)&0xff;
	hdr[3] = (lno>>8)&0xff;
	hdr[4] = lno&0xff;
	(void) write(histfd, hdr, 5);
	(void) write(histfd, cmd, strlen(cmd)+1);
	hsize = lseek(histfd, 0L, L_XTND);
	(void) flock(histfd, LOCK_UN);
	return;
bad:
	hist_finish();
}

void
hist_finish()
{
	(void) flock(histfd, LOCK_UN);
	(void) close(histfd);
	histfd = 0;
}

/*
 *	add magic to the history file
 */
static int
sprinkle(fd)
	int fd;
{
	static char mag[] = { HMAGIC1, HMAGIC2 };

	return(write(fd, mag, 2) != 2);
}

#endif
