/*
 * pg  - A clone of the System V CRT paging utility.
 *
 *	Copyright (c) 2000-2001 Gunnar Ritter. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. [deleted]
 * 4. Neither the name of Gunnar Ritter nor the names of his contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY GUNNAR RITTER AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL GUNNAR RITTER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/* Sccsid @(#)pg.c 1.44 (gritter) 2/8/02 - modified for util-linux */

/*
 * #define _XOPEN_SOURCE  500L
 *
 * Adding this define gives us the correct prototypes for fseeko, ftello,
 * but (for some glibc versions) conflicting prototype for wcwidth.
 * So, avoid defining _XOPEN_SOURCE, and give prototypes for fseeko, ftello
 * by hand.
 */

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#ifndef	TIOCGWINSZ
#include <sys/ioctl.h>
#endif
#include <sys/termios.h>
#include <fcntl.h>
#include <regex.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <ctype.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>
#include <locale.h>
#include <nl_types.h>
#include <libgen.h>
#if NCH
#include <ncurses.h>
#else
#include <curses.h>
#endif
#include <term.h>

#include "nls.h"
#include "widechar.h"
#include "../defines.h"		/* for HAVE_fseeko */

#define	READBUF		LINE_MAX	/* size of input buffer */
#define CMDBUF		255		/* size of command buffer */
#define	TABSIZE		8		/* spaces consumed by tab character */

/*
 * Avoid the message "`var' might be clobbered by `longjmp' or `vfork'"
 */
#define	CLOBBGRD(a)	(void)(&(a));

#define	cuc(c)		((c) & 0377)

enum { FORWARD = 1, BACKWARD = 2 };	/* search direction */
enum { TOP, MIDDLE, BOTTOM };		/* position of matching line */

/*
 * States for syntax-aware command line editor.
 */
enum {
	COUNT,
	SIGN,
	CMD_FIN,
	SEARCH,
	SEARCH_FIN,
	ADDON_FIN,
	STRING,
	INVALID
};

/*
 * Current command
 */
struct {
	char cmdline[CMDBUF];
	size_t cmdlen;
	int count;
	int key;
	char pattern[CMDBUF];
	char addon;
} cmd;

/*
 * Position of file arguments on argv[] to main()
 */
struct {
	int first;
	int current;
	int last;
} files;

void		(*oldint)(int);		/* old SIGINT handler */
void		(*oldquit)(int);	/* old SIGQUIT handler */
void		(*oldterm)(int);	/* old SIGTERM handler */
char		*tty;			/* result of ttyname(1) */
char		*progname;		/* program name */
unsigned	ontty;			/* whether running on tty device */
unsigned	exitstatus;		/* exit status */
int		pagelen = 23;		/* lines on a single screen page */
int		ttycols = 79;		/* screen columns (starting at 0) */
struct termios	otio;			/* old termios settings */
int		tinfostat = -1;		/* terminfo routines initialized */
int		searchdisplay = TOP;	/* matching line position */
regex_t		re;			/* regular expression to search for */
int		remembered;		/* have a remembered search string */
int		cflag;			/* clear screen before each page */
int		eflag;			/* suppress (EOF) */
int		fflag;			/* do not split lines */
int		nflag;			/* no newline for commands required */
int		rflag;			/* "restricted" pg */
int		sflag;			/* use standout mode */
char		*pstring = ":";		/* prompt string */
char		*searchfor;		/* search pattern from argv[] */
int		havepagelen;		/* page length is manually defined */
long		startline;		/* start line from argv[] */
int		nextfile = 1;		/* files to advance */
jmp_buf		jmpenv;			/* jump from signal handlers */
int		canjump;		/* jmpenv is valid */
wchar_t		wbuf[READBUF];		/* used in several widechar routines */

const char *copyright =
"@(#)pg 1.44 2/8/02. Copyright (c) 2000-2001 Gunnar Ritter. ";
const char *helpscreen = "All rights reserved.\n\
-------------------------------------------------------\n\
  h                       this screen\n\
  q or Q                  quit program\n\
  <newline>               next page\n\
  f                       skip a page forward\n\
  d or ^D                 next halfpage\n\
  l                       next line\n\
  $                       last page\n\
  /regex/                 search forward for regex\n\
  ?regex? or ^regex^      search backward for regex\n\
  . or ^L                 redraw screen\n\
  w or z                  set page size and go to next page\n\
  s filename              save current file to filename\n\
  !command                shell escape\n\
  p                       go to previous file\n\
  n                       go to next file\n\
\n\
Many commands accept preceding numbers, for example:\n\
+1<newline> (next page); -1<newline> (previous page); 1<newline> (first page).\n\
\n\
See pg(1) for more information.\n\
-------------------------------------------------------\n";

#ifdef HAVE_fseeko
#if defined (_FILE_OFFSET_BITS) && (_FILE_OFFSET_BITS) == 64
  extern int fseeko64(FILE *f, off_t off, int whence);
  extern off_t ftello64(FILE *f);
  #define      my_fseeko       fseeko64
  #define      my_ftello       ftello64
#else
  extern int fseeko(FILE *f, off_t off, int whence);
  extern off_t ftello(FILE *f);
  #define	my_fseeko	fseeko
  #define	my_ftello	ftello
#endif
#else
  static int my_fseeko(FILE *f, off_t off, int whence) {
	return fseek(f, (long) off, whence);
  }
  static off_t my_ftello(FILE *f) {
	return (off_t) ftell(f);
  }
#endif

#ifdef USE_SIGSET	/* never defined */
/* sigset and sigrelse are obsolete - use when POSIX stuff is unavailable */
#define my_sigset	sigset
#define my_sigrelse	sigrelse
#else
static int my_sigrelse(int sig) {
	sigset_t sigs;

	if (sigemptyset(&sigs) || sigaddset(&sigs, sig))
		return -1;
	return sigprocmask(SIG_UNBLOCK, &sigs, NULL);
}
typedef void (*my_sighandler_t)(int);
static my_sighandler_t my_sigset(int sig, my_sighandler_t disp) {
	struct sigaction act, oact;

	act.sa_handler = disp;
	if (sigemptyset(&act.sa_mask))
		return SIG_ERR;
	act.sa_flags = 0;
	if (sigaction(sig, &act, &oact))
		return SIG_ERR;
	if (my_sigrelse(sig))
		return SIG_ERR;
	return oact.sa_handler;
}
#endif

/*
 * Quit pg.
 */
static void
quit(int status)
{
	exit(status < 0100 ? status : 077);
}

/*
 * Memory allocator including check.
 */
static char *
smalloc(size_t s)
{
        char *m = (char *)malloc(s);
        if (m == NULL) {
                write(2, "Out of memory\n", 14);
                quit(++exitstatus);
        }
        return m;
}

/*
 * Usage message and similar routines.
 */
static void
usage(void)
{
	fprintf(stderr, _("%s: Usage: %s [-number] [-p string] [-cefnrs] "
			  "[+line] [+/pattern/] [files]\n"),
			progname, progname);
	quit(2);
}

static void
needarg(char *s)
{
	fprintf(stderr, _("%s: option requires an argument -- %s\n"),
		progname, s);
	usage();
}

static void
invopt(char *s)
{
	fprintf(stderr, _("%s: illegal option -- %s\n"), progname, s);
	usage();
}

#ifdef ENABLE_WIDECHAR
/*
 * A mbstowcs()-alike function that transparently handles invalid sequences.
 */
static size_t
xmbstowcs(wchar_t *pwcs, const char *s, size_t nwcs)
{
	size_t n = nwcs;
	int c;

	mbtowc(pwcs, NULL, MB_CUR_MAX);
	while (*s && n) {
		if ((c = mbtowc(pwcs, s, MB_CUR_MAX)) < 0) {
			s++;
			*pwcs = L'?';
		} else
			s += c;
		pwcs++;
		n--;
	}
	if (n)
		*pwcs = L'\0';
	mbtowc(pwcs, NULL, MB_CUR_MAX);
	return nwcs - n;
}
#endif

/*
 * Helper function for tputs().
 */
static int
outcap(int i)
{
	char c = i;
	return write(1, &c, 1);
}

/*
 * Write messages to terminal.
 */
static void
mesg(char *message)
{
	if (ontty == 0)
		return;
	if (*message != '\n' && sflag)
		vidputs(A_STANDOUT, outcap);
	write(1, message, strlen(message));
	if (*message != '\n' && sflag)
		vidputs(A_NORMAL, outcap);
}

/*
 * Get the window size.
 */
static void
getwinsize(void)
{
	static int initialized, envlines, envcols, deflines, defcols;
#ifdef	TIOCGWINSZ
	struct winsize winsz;
	int badioctl;
#endif
	char *p;

	if (initialized == 0) {
		if ((p = getenv("LINES")) != NULL && *p != '\0')
			if ((envlines = atoi(p)) < 0)
				envlines = 0;
		if ((p = getenv("COLUMNS")) != NULL && *p != '\0')
			if ((envcols = atoi(p)) < 0)
				envcols = 0;
		/* terminfo values. */
		if (tinfostat != 1 || columns == 0)
			defcols = 24;
		else
			defcols = columns;
		if (tinfostat != 1 || lines == 0)
			deflines = 80;
		else
			deflines = lines;
		initialized = 1;
	}
#ifdef	TIOCGWINSZ
	badioctl = ioctl(1, TIOCGWINSZ, &winsz);
#endif
	if (envcols)
		ttycols = envcols - 1;
#ifdef	TIOCGWINSZ
	else if (!badioctl)
		ttycols = winsz.ws_col - 1;
#endif
	else
		ttycols = defcols - 1;
	if (havepagelen == 0) {
		if (envlines)
			pagelen = envlines - 1;
#ifdef	TIOCGWINSZ
		else if (!badioctl)
			pagelen = winsz.ws_row - 1;
#endif
		else
			pagelen = deflines - 1;
	}
}

/*
 * Message if skipping parts of files.
 */
static void
skip(int direction)
{
	if (direction > 0)
		mesg(_("...skipping forward\n"));
	else
		mesg(_("...skipping backward\n"));
}

/*
 * Signal handler while reading from input file.
 */
static void
sighandler(int signum)
{
	if (canjump && (signum == SIGINT || signum == SIGQUIT))
		longjmp(jmpenv, signum);
	tcsetattr(1, TCSADRAIN, &otio);
	quit(exitstatus);
}

/*
 * Check whether the requested file was specified on the command line.
 */
static int
checkf(void)
{
	if (files.current + nextfile >= files.last) {
		mesg(_("No next file"));
		return 1;
	}
	if (files.current + nextfile < files.first) {
		mesg(_("No previous file"));
		return 1;
	}
	return 0;
}

#ifdef ENABLE_WIDECHAR
/*
 * Return the last character that will fit on the line at col columns
 * in case MB_CUR_MAX > 1.
 */
static char *
endline_for_mb(unsigned col, char *s)
{
	unsigned pos = 0;
	wchar_t *p = wbuf;
	wchar_t *end;
	size_t wl;
	char *t = s;

	if ((wl = xmbstowcs(wbuf, t, sizeof wbuf - 1)) == (size_t)-1)
		return s + 1;
	wbuf[wl] = L'\0';
	while (*p != L'\0') {
		switch (*p) {
			/*
			 * Cursor left.
			 */
		case L'\b':
			if (pos > 0)
				pos--;
			break;
			/*
			 * No cursor movement.
			 */
		case L'\a':
			break;
			/*
			 * Special.
			 */
		case L'\r':
			pos = 0;
			break;
		case L'\n':
			end = p + 1;
			goto ended;
			/*
			 * Cursor right.
			 */
		case L'\t':
			pos += TABSIZE - (pos % TABSIZE);
			break;
		default:
			pos += wcwidth(*p);
		}
		if (pos > col) {
			if (*p == L'\t')
				p++;
			else if (pos > col + 1)
				/*
				 * wcwidth() found a character that
				 * has multiple columns. What happens
				 * now? Assume the terminal will print
				 * the entire character onto the next
				 * row.
				 */
				p--;
			if (*++p == L'\n')
				p++;
			end = p;
			goto ended;
		}
		p++;
	}
	end = p;
 ended:
	*end = L'\0';
	p = wbuf;
	if ((pos = wcstombs(NULL, p, READBUF)) == -1)
		return s + 1;
	return s + pos;
}
#endif

/*
 * Return the last character that will fit on the line at col columns.
 */
static char *
endline(unsigned col, char *s)
{
	unsigned pos = 0;
	char *t = s;

#ifdef ENABLE_WIDECHAR
	if (MB_CUR_MAX > 1)
		return endline_for_mb(col, s);
#endif

	while (*s != '\0') {
		switch (*s) {
			/*
			 * Cursor left.
			 */
		case '\b':
			if (pos > 0)
				pos--;
			break;
			/*
			 * No cursor movement.
			 */
		case '\a':
			break;
			/*
			 * Special.
			 */
		case '\r':
			pos = 0;
			break;
		case '\n':
			t = s + 1;
			goto cend;
			/*
			 * Cursor right.
			 */
		case '\t':
			pos += TABSIZE - (pos % TABSIZE);
			break;
		default:
			pos++;
		}
		if (pos > col) {
			if (*s == '\t')
				s++;
			if (*++s == '\n')
				s++;
			t = s;
			goto cend;
		}
		s++;
	}
	t = s;
 cend:
	return t;
}

/*
 * Clear the current line on the terminal's screen.
 */
static void
cline(void)
{
	char *buf = (char *)smalloc(ttycols + 2);
	memset(buf, ' ', ttycols + 2);
	buf[0] = '\r';
	buf[ttycols + 1] = '\r';
	write(1, buf, ttycols + 2);
	free(buf);
}

/*
 * Evaluate a command character's semantics.
 */
static int
getstate(int c)
{
	switch (c) {
	case '1': case '2': case '3': case '4': case '5':
	case '6': case '7': case '8': case '9': case '0':
	case '\0':
		return COUNT;
	case '-': case '+':
		return SIGN;
	case 'l': case 'd': case '\004': case 'f': case 'z':
	case '.': case '\014': case '$': case 'n': case 'p':
	case 'w': case 'h': case 'q': case 'Q':
		return CMD_FIN;
	case '/': case '?': case '^':
		return SEARCH;
	case 's': case '!':
		return STRING;
	case 'm': case 'b': case 't':
		return ADDON_FIN;
	default:
#ifndef	PGNOBELL
		if (bell)
			tputs(bell, 1, outcap);
#endif	/* PGNOBELL */
		return INVALID;
	}
}

/*
 * Get the count and ignore last character of string.
 */
static int
getcount(char *cmdstr)
{
	char *buf;
	char *p;
	int i;

	if (*cmdstr == '\0')
		return 1;
	buf = (char *)smalloc(strlen(cmdstr) + 1);
	strcpy(buf, cmdstr);
	if (cmd.key != '\0') {
		if (cmd.key == '/' || cmd.key == '?' || cmd.key == '^') {
			if ((p = strchr(buf, cmd.key)) != NULL)
				*p = '\0';
		} else
			*(buf + strlen(buf) - 1) = '\0';
	}
	if (*buf == '\0')
		return 1;
	if (buf[0] == '-' && buf[1] == '\0') {
		i = -1;
	} else {
		if (*buf == '+')
			i = atoi(buf + 1);
		else
			i = atoi(buf);
	}
	free(buf);
	return i;
}

/*
 * Read what the user writes at the prompt. This is tricky because
 * we check for valid input.
 */
static void
prompt(long long pageno)
{
	struct termios tio;
	char key;
	int state = COUNT;
	int escape = 0;
	char b[LINE_MAX], *p;

	if (pageno != -1) {
		if ((p = strstr(pstring, "%d")) == NULL) {
			mesg(pstring);
		} else {
			strcpy(b, pstring);
			sprintf(b + (p - pstring), "%lld", pageno);
			strcat(b, p + 2);
			mesg(b);
		}
	}
	cmd.key = cmd.addon = cmd.cmdline[0] = '\0';
	cmd.cmdlen = 0;
	tcgetattr(1, &tio);
	tio.c_lflag &= ~(ICANON | ECHO);
	tio.c_cc[VMIN] = 1;
	tio.c_cc[VTIME] = 0;
	tcsetattr(1, TCSADRAIN, &tio);
	tcflush(1, TCIFLUSH);
	for (;;) {
		switch (read(1, &key, 1)) {
		case 0: quit(0);
			/*NOTREACHED*/
		case -1: quit(1);
		}
		if (key == tio.c_cc[VERASE]) {
			if (cmd.cmdlen) {
				write(1, "\b \b", 3);
				cmd.cmdline[--cmd.cmdlen] = '\0';
				switch (state) {
				case ADDON_FIN:
					state = SEARCH_FIN;
					cmd.addon = '\0';
					break;
				case CMD_FIN:
					cmd.key = '\0';
					state = COUNT;
					break;
				case SEARCH_FIN:
					state = SEARCH;
					/*FALLTHRU*/
				case SEARCH:
					if (cmd.cmdline[cmd.cmdlen - 1]
							== '\\') {
						escape = 1;
						while(cmd.cmdline[cmd.cmdlen
								- escape - 1]
							== '\\') escape++;
						escape %= 2;
					}
					else {
						escape = 0;
						if (strchr(cmd.cmdline, cmd.key)
							== NULL) {
							cmd.key = '\0';
							state = COUNT;
						}
					}
					break;
				}
			}
			if (cmd.cmdlen == 0) {
				state = COUNT;
				cmd.key = '\0';
			}
			continue;
		}
		if (key == tio.c_cc[VKILL]) {
			cline();
			cmd.cmdlen = 0;
			cmd.cmdline[0] = '\0';
			state = COUNT;
			cmd.key = '\0';
			continue;
		}
		if (key == '\n' || (nflag && state == COUNT && key == ' '))
			break;
		if (cmd.cmdlen >= CMDBUF - 1)
			continue;
		switch (state) {
		case STRING:
			break;
		case SEARCH:
			if (!escape) {
				if (key == cmd.key)
					state = SEARCH_FIN;
				if (key == '\\')
					escape = 1;
			} else
				escape = 0;
			break;
		case SEARCH_FIN:
			if (getstate(key) != ADDON_FIN)
				continue;
			state = ADDON_FIN;
			cmd.addon = key;
			switch (key) {
			case 't':
				searchdisplay = TOP;
				break;
			case 'm':
				searchdisplay = MIDDLE;
				break;
			case 'b':
				searchdisplay = BOTTOM;
				break;
			}
			break;
		case CMD_FIN:
		case ADDON_FIN:
			continue;
		default:
			state = getstate(key);
			switch (state) {
			case SIGN:
				if (cmd.cmdlen != 0) {
					state = INVALID;
					continue;
				}
				state = COUNT;
				/*FALLTHRU*/
			case COUNT:
				break;
			case ADDON_FIN:
			case INVALID:
				continue;
			default:
				cmd.key = key;
			}
		}
		write(1, &key, 1);
		cmd.cmdline[cmd.cmdlen++] = key;
		cmd.cmdline[cmd.cmdlen] = '\0';
		if (nflag && state == CMD_FIN)
			goto endprompt;
	}
endprompt:
	tcsetattr(1, TCSADRAIN, &otio);
	cline();
	cmd.count = getcount(cmd.cmdline);
}

#ifdef ENABLE_WIDECHAR
/*
 * Remove backspace formatting, for searches
 * in case MB_CUR_MAX > 1.
 */
static char *
colb_for_mb(char *s)
{
	char *p = s;
	wchar_t *wp, *wq;
	size_t l = strlen(s), wl;
	unsigned i;

	if ((wl = xmbstowcs(wbuf, p, sizeof wbuf)) == (size_t)-1)
		return s;
	for (wp = wbuf, wq = wbuf, i = 0; *wp != L'\0' && i < wl;
	     wp++, wq++) {
		if (*wp == L'\b') {
			if (wq != wbuf)
				wq -= 2;
			else
				wq--;
		} else
			*wq = *wp;
	}
	*wq = L'\0';
	wp = wbuf;
	wcstombs(s, wp, l + 1);

	return s;
}
#endif

/*
 * Remove backspace formatting, for searches.
 */
static char *
colb(char *s)
{
	char *p = s, *q;

#ifdef ENABLE_WIDECHAR
	if (MB_CUR_MAX > 1)
		return colb_for_mb(s);
#endif

	for (q = s; *p != '\0'; p++, q++) {
		if (*p == '\b') {
			if (q != s)
				q -= 2;
			else
				q--;
		} else
			*q = *p;
	}
	*q = '\0';

	return s;
}

#ifdef ENABLE_WIDECHAR
/*
 * Convert nonprintable characters to spaces
 * in case MB_CUR_MAX > 1.
 */
static void
makeprint_for_mb(char *s, size_t l)
{
	char *t = s;
	wchar_t *wp = wbuf;
	size_t wl;

	if ((wl = xmbstowcs(wbuf, t, sizeof wbuf)) == (size_t)-1)
		return;
	while (wl--) {
		if (!iswprint(*wp) && *wp != L'\n' && *wp != L'\r'
		    && *wp != L'\b' && *wp != L'\t')
			*wp = L'?';
		wp++;
	}
	wp = wbuf;
	wcstombs(s, wp, l);
}
#endif

/*
 * Convert nonprintable characters to spaces.
 */
static void
makeprint(char *s, size_t l)
{
#ifdef ENABLE_WIDECHAR
	if (MB_CUR_MAX > 1)
		return makeprint_for_mb(s, l);
#endif

	while (l--) {
		if (!isprint(cuc(*s)) && *s != '\n' && *s != '\r'
		    && *s != '\b' && *s != '\t')
			*s = '?';
		s++;
	}
}

/*
 * Strip backslash characters from the given string.
 */
static void
striprs(char *s)
{
	char *p = s;

	do {
		if (*s == '\\') {
			s++;
		}
		*p++ = *s;
	} while (*s++ != '\0');
}

/*
 * Extract the search pattern off the command line.
 */
static char *
makepat(void)
{
	char *p;

	if (cmd.addon == '\0')
		p = cmd.cmdline + strlen(cmd.cmdline) - 1;
	else
		p = cmd.cmdline + strlen(cmd.cmdline) - 2;
	if (*p == cmd.key)
		*p = '\0';
	else
		*(p + 1) = '\0';
	if ((p = strchr(cmd.cmdline, cmd.key)) != NULL) {
		p++;
		striprs(p);
	}
	return p;
}

/*
 * Process errors that occurred in temporary file operations.
 */
static void
tmperr(FILE *f, char *ftype)
{
	if (ferror(f))
		fprintf(stderr, _("%s: Read error from %s file\n"),
			progname, ftype);
	else if (feof(f))
		/*
		 * Most likely '\0' in input.
		 */
		fprintf(stderr, _("%s: Unexpected EOF in %s file\n"),
			progname, ftype);
	else
		fprintf(stderr, _("%s: Unknown error in %s file\n"),
			progname, ftype);
	quit(++exitstatus);
}

/*
 * perror()-like, but showing the program's name.
 */
static void
pgerror(int eno, char *string)
{
	fprintf(stderr, "%s: %s: %s\n", progname, string, strerror(eno));
}

/*
 * Read the file and respond to user input.
 * Beware: long and ugly.
 */
static void
pgfile(FILE *f, char *name)
{
	off_t pos, oldpos, fpos;
	off_t line = 0, fline = 0, bline = 0, oldline = 0, eofline = 0;
	int dline = 0;
	/*
	 * These are the line counters:
	 * line		the line desired to display
	 * fline	the current line of the input file
	 * bline	the current line of the file buffer
	 * oldline	the line before a search was started
	 * eofline	the last line of the file if it is already reached
	 * dline	the line on the display
	 */
	int search = 0;
	unsigned searchcount = 0;
	/*
	 * Advance to EOF immediately.
	 */
	int seekeof = 0;
	/*
	 * EOF has been reached by `line'.
	 */
	int eof = 0;
	/*
	 * f and fbuf refer to the same file.
	 */
	int nobuf = 0;
	int sig;
	int rerror;
	size_t sz;
	char b[READBUF + 1];
	char *p;
	/*
	 * fbuf		an exact copy of the input file as it gets read
	 * find		index table for input, one entry per line
	 * save		for the s command, to save to a file
	 */
	FILE *fbuf, *find, *save;

	/* silence compiler - it may warn about longjmp() */
	CLOBBGRD(line);
	CLOBBGRD(fline);
	CLOBBGRD(bline);
	CLOBBGRD(oldline);
	CLOBBGRD(eofline);
	CLOBBGRD(dline);
	CLOBBGRD(ttycols);
	CLOBBGRD(search);
	CLOBBGRD(searchcount);
	CLOBBGRD(seekeof);
	CLOBBGRD(eof);
	CLOBBGRD(fpos);
	CLOBBGRD(nobuf);
	CLOBBGRD(fbuf);

	if (ontty == 0) {
		/*
		 * Just copy stdin to stdout.
		 */
		while ((sz = fread(b, sizeof *b, READBUF, f)) != 0)
			write(1, b, sz);
		if (ferror(f)) {
			pgerror(errno, name);
			exitstatus++;
		}
		return;
	}
	if ((fpos = my_fseeko(f, (off_t)0, SEEK_SET)) == -1)
		fbuf = tmpfile();
	else {
		fbuf = f;
		nobuf = 1;
	}
	find = tmpfile();
	if (fbuf == NULL || find == NULL) {
		fprintf(stderr, _("%s: Cannot create tempfile\n"), progname);
		quit(++exitstatus);
	}
	if (searchfor) {
		search = FORWARD;
		oldline = 0;
		searchcount = 1;
		rerror = regcomp(&re, searchfor, REG_NOSUB | REG_NEWLINE);
		if (rerror != 0) {
			mesg(_("RE error: "));
			regerror(rerror, &re, b, READBUF);
			mesg(b);
			goto newcmd;
		}
		remembered = 1;
	}

	for (line = startline; ; ) {
		/*
		 * Get a line from input file or buffer.
		 */
		if (line < bline) {
			my_fseeko(find, line * sizeof pos, SEEK_SET);
			if (fread(&pos, sizeof pos, 1, find) == 0)
				tmperr(find, "index");
			my_fseeko(find, (off_t)0, SEEK_END);
			my_fseeko(fbuf, pos, SEEK_SET);
			if (fgets(b, READBUF, fbuf) == NULL)
				tmperr(fbuf, "buffer");
		} else if (eofline == 0) {
			my_fseeko(find, (off_t)0, SEEK_END);
			do {
				if (!nobuf)
					my_fseeko(fbuf, (off_t)0, SEEK_END);
				pos = my_ftello(fbuf);
				if ((sig = setjmp(jmpenv)) != 0) {
					/*
					 * We got a signal.
					 */
					canjump = 0;
					my_sigrelse(sig);
					my_fseeko(fbuf, pos, SEEK_SET);
					*b = '\0';
					dline = pagelen;
					break;
				} else {
					if (nobuf)
						my_fseeko(f, fpos, SEEK_SET);
					canjump = 1;
					p = fgets(b, READBUF, f);
					if (nobuf)
						if ((fpos = my_ftello(f)) == -1)
							pgerror(errno, name);
					canjump = 0;
				}
				if (p == NULL || *b == '\0') {
					if (ferror(f))
						pgerror(errno, name);
					eofline = fline;
					eof = 1;
					break;
				} else {
					if (!nobuf)
						fputs(b, fbuf);
					fwrite(&pos, sizeof pos, 1, find);
					if (!fflag) {
						oldpos = pos;
						p = b;
						while (*(p = endline(ttycols,
									p))
								!= '\0') {
							pos = oldpos + (p - b);
							fwrite(&pos,
								sizeof pos,
								1, find);
							fline++;
							bline++;
						}
					}
					fline++;
				}
			} while (line > bline++);
		} else {
			/*
			 * eofline != 0
			 */
			eof = 1;
		}
		if (search == FORWARD) {
			if (eof) {
				line = oldline;
				search = searchcount = 0;
				mesg(_("Pattern not found"));
				eof = 0;
				goto newcmd;
			}
			line++;
			colb(b);
			if (regexec(&re, b, 0, NULL, 0) == 0) {
				searchcount--;
			}
			if (searchcount == 0) {
				search = dline = 0;
				switch (searchdisplay) {
				case TOP:
					line -= 1;
					break;
				case MIDDLE:
					line -= pagelen / 2 + 1;
					break;
				case BOTTOM:
					line -= pagelen;
					break;
				}
				skip(1);
			}
			continue;
		} else if (eof)	{	/*
					 * We are not searching.
					 */
			line = bline;
		} else if (*b != '\0') {
			if (cflag && clear_screen) {
				switch (dline) {
				case 0:
					tputs(clear_screen, 1, outcap);
					dline = 0;
				}
			}
			line++;
			if (eofline && line == eofline)
				eof = 1;
			dline++;
			if ((sig = setjmp(jmpenv)) != 0) {
				/*
				 * We got a signal.
				 */
				canjump = 0;
				my_sigrelse(sig);
				dline = pagelen;
			} else {
				p = endline(ttycols, b);
				sz = p - b;
				makeprint(b, sz);
				canjump = 1;
				write(1, b, sz);
				canjump = 0;
			}
		}
		if (dline >= pagelen || eof) {
			/*
			 * Time for prompting!
			 */
			if (eof && seekeof) {
				eof = seekeof = 0;
				if (line >= pagelen)
					line -= pagelen;
				else
					line = 0;
				dline = -1;
				continue;
			}
newcmd:
			if (eof) {
				if (fline == 0 || eflag)
					break;
				mesg(_("(EOF)"));
			}
			prompt((line - 1) / pagelen + 1);
			switch (cmd.key) {
			case '/':
				/*
				 * Search forward.
				 */
				search = FORWARD;
				oldline = line;
				searchcount = cmd.count;
				p = makepat();
				if (p != NULL && *p) {
					if (remembered == 1)
						regfree(&re);
					rerror = regcomp(&re, p,
						REG_NOSUB | REG_NEWLINE);
					if (rerror != 0) {
						mesg(_("RE error: "));
						sz = regerror(rerror, &re,
								b, READBUF);
						mesg(b);
						goto newcmd;
					}
					remembered = 1;
				} else if (remembered == 0) {
					mesg(_("No remembered search string"));
					goto newcmd;
				}
				continue;
			case '?':
			case '^':
				/*
				 * Search backward.
				 */
				search = BACKWARD;
				oldline = line;
				searchcount = cmd.count;
				p = makepat();
				if (p != NULL && *p) {
					if (remembered == 1)
						regfree(&re);
					rerror = regcomp(&re, p,
						REG_NOSUB | REG_NEWLINE);
					if (rerror != 0) {
						mesg("RE error: ");
						regerror(rerror, &re,
								b, READBUF);
						mesg(b);
						goto newcmd;
					}
					remembered = 1;
				} else if (remembered == 0) {
					mesg("No remembered search string");
					goto newcmd;
				}
				line -= pagelen;
				if (line <= 0)
					goto notfound_bw;
				while (line) {
					my_fseeko(find, --line * sizeof pos,
							SEEK_SET);
					if(fread(&pos, sizeof pos, 1,find)==0)
						tmperr(find, "index");
					my_fseeko(find, (off_t)0, SEEK_END);
					my_fseeko(fbuf, pos, SEEK_SET);
					if (fgets(b, READBUF, fbuf) == NULL)
						tmperr(fbuf, "buffer");
					colb(b);
					if (regexec(&re, b, 0, NULL, 0) == 0)
						searchcount--;
					if (searchcount == 0)
						goto found_bw;
				}
notfound_bw:
				line = oldline;
				search = searchcount = 0;
				mesg(_("Pattern not found"));
				goto newcmd;
found_bw:
				eof = search = dline = 0;
				skip(-1);
				switch (searchdisplay) {
				case TOP:
					/* line -= 1; */
					break;
				case MIDDLE:
					line -= pagelen / 2;
					break;
				case BOTTOM:
					if (line != 0)
						dline = -1;
					line -= pagelen;
					break;
				}
				if (line < 0)
					line = 0;
				continue;
			case 's':
				/*
				 * Save to file.
				 */
				p = cmd.cmdline;
				while (*++p == ' ');
				if (*p == '\0')
					goto newcmd;
				save = fopen(p, "wb");
				if (save == NULL) {
					cmd.count = errno;
					mesg(_("Cannot open "));
					mesg(p);
					mesg(": ");
					mesg(strerror(cmd.count));
					goto newcmd;
				}
				/*
				 * Advance to EOF.
				 */
				my_fseeko(find, (off_t)0, SEEK_END);
				for (;;) {
					if (!nobuf)
						my_fseeko(fbuf,(off_t)0,SEEK_END);
					pos = my_ftello(fbuf);
					if (fgets(b, READBUF, f) == NULL) {
						eofline = fline;
						break;
					}
					if (!nobuf)
						fputs(b, fbuf);
					fwrite(&pos, sizeof pos, 1, find);
					if (!fflag) {
						oldpos = pos;
						p = b;
						while (*(p = endline(ttycols,
									p))
								!= '\0') {
							pos = oldpos + (p - b);
							fwrite(&pos,
								sizeof pos,
								1, find);
							fline++;
							bline++;
						}
					}
					fline++;
					bline++;
				}
				my_fseeko(fbuf, (off_t)0, SEEK_SET);
				while ((sz = fread(b, sizeof *b, READBUF,
							fbuf)) != 0) {
					/*
					 * No error check for compat.
					 */
					fwrite(b, sizeof *b, sz, save);
				}
				fclose(save);
				my_fseeko(fbuf, (off_t)0, SEEK_END);
				mesg(_("saved"));
				goto newcmd;
			case 'l':
				/*
				 * Next line.
				 */
				if (*cmd.cmdline != 'l')
					eof = 0;
				if (cmd.count == 0)
					cmd.count = 1; /* compat */
				if (isdigit(cuc(*cmd.cmdline))) {
					line = cmd.count - 2;
					dline = 0;
				} else {
					if (cmd.count != 1) {
						line += cmd.count - 1
							- pagelen;
						dline = -1;
						skip(cmd.count);
					}
					/*
					 * Nothing to do if count==1.
					 */
				}
				break;
			case 'd':
				/*
				 * Half screen forward.
				 */
			case '\004':	/* ^D */
				if (*cmd.cmdline != cmd.key)
					eof = 0;
				if (cmd.count == 0)
					cmd.count = 1; /* compat */
				line += (cmd.count * pagelen / 2)
					- pagelen - 1;
				dline = -1;
				skip(cmd.count);
				break;
			case 'f':
				/*
				 * Skip forward.
				 */
				if (cmd.count <= 0)
					cmd.count = 1; /* compat */
				line += cmd.count * pagelen - 2;
				if (eof)
					line += 2;
				if (*cmd.cmdline != 'f')
					eof = 0;
				else if (eof)
					break;
				if (eofline && line >= eofline)
					line -= pagelen;
				dline = -1;
				skip(cmd.count);
				break;
			case '\0':
				/*
				 * Just a number, or '-', or <newline>.
				 */
				if (cmd.count == 0)
					cmd.count = 1; /* compat */
				if (isdigit(cuc(*cmd.cmdline)))
					line = (cmd.count - 1) * pagelen - 2;
				else
					line += (cmd.count - 1)
						* (pagelen - 1) - 2;
				if (*cmd.cmdline != '\0')
					eof = 0;
				if (cmd.count != 1) {
					skip(cmd.count);
					dline = -1;
				} else {
					dline = 1;
					line += 2;
				}
				break;
			case '$':
				/*
				 * Advance to EOF.
				 */
				if (!eof)
					skip(1);
				eof = 0;
				line = LONG_MAX;
				seekeof = 1;
				dline = -1;
				break;
			case '.':
			case '\014': /* ^L */
				/*
				 * Repaint screen.
				 */
				eof = 0;
				if (line >= pagelen)
					line -= pagelen;
				else
					line = 0;
				dline = 0;
				break;
			case '!':
				/*
				 * Shell escape.
				 */
				if (rflag) {
					mesg(progname);
					mesg(_(": !command not allowed in "
					       "rflag mode.\n"));
				} else {
					pid_t cpid;

					write(1, cmd.cmdline,
					      strlen(cmd.cmdline));
					write(1, "\n", 1);
					my_sigset(SIGINT, SIG_IGN);
					my_sigset(SIGQUIT, SIG_IGN);
					switch (cpid = fork()) {
					case 0:
						p = getenv("SHELL");
						if (p == NULL) p = "/bin/sh";
						if (!nobuf)
							fclose(fbuf);
						fclose(find);
						if (isatty(0) == 0) {
							close(0);
							open(tty, O_RDONLY);
						} else {
							fclose(f);
						}
						my_sigset(SIGINT, oldint);
						my_sigset(SIGQUIT, oldquit);
						my_sigset(SIGTERM, oldterm);
						execl(p, p, "-c",
							cmd.cmdline + 1, NULL);
						pgerror(errno, p);
						_exit(0177);
						/*NOTREACHED*/
					case -1:
						mesg(_("fork() failed, "
						       "try again later\n"));
						break;
					default:
						while (wait(NULL) != cpid);
					}
					my_sigset(SIGINT, sighandler);
					my_sigset(SIGQUIT, sighandler);
					mesg("!\n");
				}
				goto newcmd;
			case 'h':
				/*
				 * Help!
				 */
				write(1, copyright + 4, strlen(copyright + 4));
				write(1, helpscreen, strlen(helpscreen));
				goto newcmd;
			case 'n':
				/*
				 * Next file.
				 */
				if (cmd.count == 0)
					cmd.count = 1;
				nextfile = cmd.count;
				if (checkf()) {
					nextfile = 1;
					goto newcmd;
				}
				eof = 1;
				break;
			case 'p':
				/*
				 * Previous file.
				 */
				if (cmd.count == 0)
					cmd.count = 1;
				nextfile = 0 - cmd.count;
				if (checkf()) {
					nextfile = 1;
					goto newcmd;
				}
				eof = 1;
				break;
			case 'q':
			case 'Q':
				/*
				 * Exit pg.
				 */
				quit(exitstatus);
				/*NOTREACHED*/
			case 'w':
			case 'z':
				/*
				 * Set window size.
				 */
				if (cmd.count < 0)
					cmd.count = 0;
				if (*cmd.cmdline != cmd.key)
					pagelen = ++cmd.count;
				dline = 1;
				break;
			}
			if (line <= 0) {
				line = 0;
				dline = 0;
			}
			if (cflag && dline == 1) {
				dline = 0;
				line--;
			}
		}
		if (eof)
			break;
	}
	fclose(find);
	if (!nobuf)
		fclose(fbuf);
}

int
main(int argc, char **argv)
{
	int arg, i;
	char *p;
	FILE *input;

	progname = basename(argv[0]);

	setlocale(LC_MESSAGES, "");
	bindtextdomain(PACKAGE, LOCALEDIR);
	textdomain(PACKAGE);

	if (tcgetattr(1, &otio) == 0) {
		ontty = 1;
		oldint = my_sigset(SIGINT, sighandler);
		oldquit = my_sigset(SIGQUIT, sighandler);
		oldterm = my_sigset(SIGTERM, sighandler);
		setlocale(LC_CTYPE, "");
		setlocale(LC_COLLATE, "");
		tty = ttyname(1);
		setupterm(NULL, 1, &tinfostat);
		getwinsize();
		helpscreen = _(helpscreen);
	}
	for (arg = 1; argv[arg]; arg++) {
		if (*argv[arg] == '+')
			continue;
		if (*argv[arg] != '-' || argv[arg][1] == '\0')
			break;
		argc--;
		for (i = 1; argv[arg][i]; i++) {
			switch (argv[arg][i]) {
			case '-':
				if (i != 1 || argv[arg][i + 1])
					invopt(&argv[arg][i]);
				goto endargs;
			case '1': case '2': case '3': case '4': case '5':
			case '6': case '7': case '8': case '9': case '0':
				pagelen = atoi(argv[arg] + i);
				havepagelen = 1;
				goto nextarg;
			case 'c':
				cflag = 1;
				break;
			case 'e':
				eflag = 1;
				break;
			case 'f':
				fflag = 1;
				break;
			case 'n':
				nflag = 1;
				break;
			case 'p':
				if (argv[arg][i + 1]) {
					pstring = &argv[arg][i + 1];
				} else if (argv[++arg]) {
					--argc;
					pstring = argv[arg];
				} else
					needarg("-p");
				goto nextarg;
			case 'r':
				rflag = 1;
				break;
			case 's':
				sflag = 1;
				break;
			default:
				invopt(&argv[arg][i]);
			}
		}
nextarg:
		;
	}
endargs:
	for (arg = 1; argv[arg]; arg++) {
		if (*argv[arg] == '-') {
			if (argv[arg][1] == '-') {
				arg++;
				break;
			}
			if (argv[arg][1] == '\0')
				break;
			if (argv[arg][1] == 'p' && argv[arg][2] == '\0')
				arg++;
			continue;
		}
		if (*argv[arg] != '+')
			break;
		argc--;
		switch (*(argv[arg] + 1)) {
		case '\0':
			needarg("+");
			/*NOTREACHED*/
		case '1': case '2': case '3': case '4': case '5':
		case '6': case '7': case '8': case '9': case '0':
			startline = atoi(argv[arg] + 1);
			break;
		case '/':
			searchfor = argv[arg] + 2;
			if (*searchfor == '\0')
				needarg("+/");
			p = searchfor + strlen(searchfor) - 1;
			if (*p == '/') *p = '\0';
			if (*searchfor == '\0')
				needarg("+/");
			break;
		default:
			invopt(argv[arg]);
		}
	}
	if (argc == 1) {
		pgfile(stdin, "stdin");
	} else {
		files.first = arg;
		files.last = arg + argc - 1;
		for ( ; argv[arg]; arg += nextfile) {
			nextfile = 1;
			files.current = arg;
			if (argc > 2) {
				static int firsttime;
				firsttime++;
				if (firsttime > 1) {
					mesg(_("(Next file: "));
					mesg(argv[arg]);
					mesg(")");
newfile:
					if (ontty) {
					prompt(-1);
					switch(cmd.key) {
					case 'n':
						/*
					 	* Next file.
					 	*/
						if (cmd.count == 0)
							cmd.count = 1;
						nextfile = cmd.count;
						if (checkf()) {
							nextfile = 1;
							mesg(":");
							goto newfile;
						}
						continue;
					case 'p':
						/*
					 	* Previous file.
					 	*/
						if (cmd.count == 0)
							cmd.count = 1;
						nextfile = 0 - cmd.count;
						if (checkf()) {
							nextfile = 1;
							mesg(":");
							goto newfile;
						}
						continue;
					case 'q':
					case 'Q':
						quit(exitstatus);
				}
				} else mesg("\n");
				}
			}
			if (strcmp(argv[arg], "-") == 0)
				input = stdin;
			else {
				input = fopen(argv[arg], "r");
				if (input == NULL) {
					pgerror(errno, argv[arg]);
					exitstatus++;
					continue;
				}
			}
			if (ontty == 0 && argc > 2) {
				/*
				 * Use the prefix as specified by SUSv2.
				 */
				write(1, "::::::::::::::\n", 15);
				write(1, argv[arg], strlen(argv[arg]));
				write(1, "\n::::::::::::::\n", 16);
			}
			pgfile(input, argv[arg]);
			if (input != stdin)
				fclose(input);
		}
	}
	quit(exitstatus);
	/*NOTREACHED*/
	return 0;
}
