/*
	history.c -- primitive history mechanism

	Paul Haahr & Byron Rakitzis, July 1991.

	This program mimics the att v8 = and == history programs.
	The edit() algorithm was adapted from a similar program
	that Boyd Roberts wrote, but otherwise all the code has
	been written from scratch.

	edit() was subsequently redone by Hugh Redelmeier in order
	to correctly deal with tab characters in the source line.

	BUGS:
	There is an implicit assumption that commands are no
	more than 1k characters long. 
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static char *id = "@(#) history.c  8/91";

#undef FALSE
#undef TRUE
typedef enum { FALSE, TRUE } bool;

#define CHUNKSIZE 65536

static struct {
	char *old, *new;
} *replace;

static char **search, *progname, *history;
static char me;	/* typically ':' or '-' */
static bool editit = FALSE, printit = FALSE;
static int nreplace = 0, nsearch = 0;
static FILE *fp;

static void *ealloc(size_t n) {
	void *p = (void *) malloc(n);
	if (p == NULL) {
		perror("malloc");
		exit(1);
	}
	return p;
}

static void *erealloc(void *p, size_t n) {
	p = (void *) realloc(p, n);
	if (p == NULL) {
		perror("realloc");
		exit(1);
	}
	return p;
}

static char *newstr() {
	return ealloc((size_t)1024);
}

static char *basename(char *s) {
	char *t = strrchr(s, '/');
	return (t == NULL) ? s : t + 1;
}

/* stupid O(n^2) substring matching routine */

static char *isin(char *target, char *pattern) {
	size_t plen = strlen(pattern);
	size_t tlen = strlen(target);
	for (; tlen >= plen; target++, --tlen)
		if (strncmp(target, pattern, plen) == 0)
			return target;
	return NULL;
}

/* replace the first match in the string with "new" */
static char *sub(char *s, char *old, char *new) {
	char *t, *u;

	t = isin(s, old);
	u = newstr();

	*t = '\0';
	while (*old != '\0')
		old++, t++;
	strcpy(u, s);
	strcat(u, new);
	strcat(u, t);
	return u;
}

static char *edit(char *s) {
	char *final, *f, *end;
	int col;
	bool ins;
	
start:
	fprintf(stderr, "%s\n", s);	
	f = final = newstr();
	end = s + strlen(s);
	col = 0;
	ins = FALSE;
	
	for (;; col++) {
		int	c = getchar();

		if (c == me && col == 0) {
			int	peekc = getchar();
			if (peekc == '\n')
				return NULL;
			ungetc(peekc, stdin);
		}
		if (c == '\n') {
			if (col == 0)
				return s;
			
			while (s < end) /* copy remainder of string */
				*f++ = *s++;
			*f = '\0';
			s = final;
			goto start;
		} else if (ins || s>=end) {
			/* col need not be accurate -- tabs need not be interpreted */
			*f++ = c;
		} else {
			switch (c) {
			case '+':
				while (s < end)
					*f++ = *s++;
				*f = '\0';
				continue;
			case '%':
				c = ' ';
				/* FALLTHROUGH */
			default:
				*f++ = c;
				break;	
			case EOF:
				exit(1);
				/* NOTREACHED */
			case ' ':
				if (*s == '\t') {
					int	oldcol = col;

					for (;; col++) {
						int	peekc;

						if ((col&07) == 07) {
							*f++ = '\t';	/* we spaced past a tab */
							break;
						}
						peekc = getchar();
						if (peekc != ' ') {
							ungetc(peekc, stdin);
							if (peekc != '\n') {
								/* we spaced partially into a tab */
								do {
									*f++ = ' ';
									oldcol++;
								} while (oldcol <= col);
							}
							break;
						}
					}
				} else {
					*f++ = *s;
				}
				break;
			case '#':
				break;
			case '$':
				end = s;	/* truncate s */
				continue;	/* skip incrementing s */
			case '^':
				ins = TRUE;
				continue;	/* skip incrementing s */
			case '\t':
				for (;; col++) {
					if ((*f++ = s<end? *s++ : '\t') == '\t') {
						col = col | 07;	/* advance to before next tabstop */
					}
					if ((col&07) == 07)	/* stop before tabstop */
						break;
				}
				continue;	/* skip incrementing s */
			}
			if (s<end && (*s!='\t' || (col&07)==07))
				s++;
		}
	}
}

static char *readhistoryfile(char **last) {
	char *buf;
	size_t count, size;
	long nread;

	if ((history = getenv("history")) == NULL) {
		fprintf(stderr, "$history not set\n");
		exit(1);
	}
	fp = fopen(history, "r+");
	if (fp == NULL) {
		perror(history);
		exit(1);
	}

	size = 0;
	count = 0;
	buf = ealloc(size = CHUNKSIZE);
	while ((nread = fread(buf + count, sizeof (char), size - count, fp)) > 0) {
		count += nread;
		if (size - count == 0)
			buf = erealloc(buf, size *= 4);
	}
	if (nread == -1) {
		perror(history);
		exit(1);
	}
	*last = buf + count;
	return buf;
}

static char *getcommand() {
	char *s, *t;
	static char *hist = NULL, *last;

	if (hist == NULL) {
		hist = readhistoryfile(&last);
		*--last = '\0';		/* trim final newline */
	}

again:	s = last;
	if (s < hist)
		return NULL;
	while (*--s != '\n')
		if (s <= hist) {
			last = hist - 1;
			return hist;
		}
	*s = '\0';
	last = s++;

	/*
	 * if the command contains the "me" character at the start of the line
	 * or after any of [`{|()@] then try again
	 */

	for (t = s; *t != '\0'; t++)
		if (*t == me) {
			char *u = t - 1;
			while (u >= s && (*u == ' ' || *u == '\t'))
				--u;
			if (u < s)
				goto again;
			switch (*u) {
			case '`': case '@':
			case '(': case ')':
			case '{': case '|':
				goto again;
			default:
				break;
			}
		}
	return s;
}

int main(int argc, char **argv) {
	int i;
	char *s;

	s = progname = basename(argv[0]);
	me = *s++;
	if (*s == me) {
		s++;
		editit = TRUE;
	}
	if (*s == 'p') {
		s++;
		printit = TRUE;
	}
/* Nahh...
	if (*s != '\0') {
		fprintf(stderr, "\"%s\": bad name for history program\n", progname);
		exit(1);
	}
*/

	if (argc > 1) {
		replace = ealloc((argc - 1) * sizeof *replace);
		search = ealloc((argc - 1) * sizeof *search);
	}
	for (i = 1; i < argc; i++)
		if ((s = strchr(argv[i], ':')) == NULL)
			search[nsearch++] = argv[i];
		else {
			*(char *)s = '\0';	/* do we confuse ps too much? */
			replace[nreplace].old = argv[i];
			replace[nreplace].new = s + 1;
			nreplace++;
		}

next:	s = getcommand();
	if (s == NULL) {
		fprintf(stderr, "command not matched\n");
		return 1;
	}
	for (i = 0; i < nsearch; i++)
		if (!isin(s, search[i]))
			goto next;
	for (i = 0; i < nreplace; i++)
		if (!isin(s, replace[i].old))
			goto next;
		else
			s = sub(s, replace[i].old, replace[i].new);
	if (editit) {
		s = edit(s);
		if (s == NULL)
			goto next;
	}
	fseek(fp, 0, 2); /* 2 == end of file. i.e., append command to $history */
	fprintf(fp, "%s\n", s);
	fclose(fp);
	if (printit)
		printf("%s\n", s);
	else {
		char *shell = getenv("SHELL");

		if (!editit)
			fprintf(stderr, "%s\n", s);
		if (shell == NULL)
			shell = "/bin/sh";
		execl(shell, basename(shell), "-c", s, NULL);
		perror(shell);
		exit(1);
	}
	return 0;
}
