#include <u.h>
#include <libc.h>
#include <bio.h>
#include <ctype.h>

/*
 *	PR command (print files in pages and columns, with headings)
 *	2+head+2+page[56]+5
 */

#define	ISPRINT(c)	((c) >= ' ')
#define ESC		'\033'
#define LENGTH		66
#define LINEW		72
#define NUMW		5
#define MARGIN		10
#define DEFTAB		8
#define NFILES		20
#define HEAD		"%12.12s %4.4s  %s Page %d\n\n\n", date+4, date+24, head, Page
#define TOLOWER(c)	(isupper(c) ? tolower(c) : c)	/* ouch! */
#define cerror(S)	fprint(2, "pr: %s", S)
#define STDINNAME()	nulls
#define TTY		"/dev/cons", 0
#define PROMPT()	fprint(2, "\a") /* BEL */
#define TABS(N,C)	if((N = intopt(argv, &C)) < 0) N = DEFTAB
#define ETABS		(Inpos % Etabn)
#define ITABS		(Itabn > 0 && Nspace > 1 && Nspace >= (nc = Itabn - Outpos % Itabn))
#define NSEPC		'\t'
#define EMPTY		14	/* length of " -- empty file" */

typedef	struct	Fils	Fils;
typedef	struct	Colp*	Colp;
typedef	struct	Err	Err;

struct	Fils
{
	Biobuf*	f_f;
	char*	f_name;
	long	f_nextc;
};
struct	Colp
{
	Rune*	c_ptr;
	Rune*	c_ptr0;
	long	c_lno;
};
struct	Err
{
	Err*	e_nextp;
	char*	e_mess;
};

int	Balance = 0;
Biobuf	bout;
Rune*	Bufend;
Rune*	Buffer = 0;
int	C = '\0';
Colp	Colpts;
int	Colw;
int	Dblspace = 1;
Err*	err = 0;
int	error = 0;
int	Etabc = '\t';
int	Etabn = 0;
Fils*	Files;
int	Formfeed = 0;
int	Fpage = 1;
char*	Head = 0;
int	Inpos;
int	Itabc = '\t';
int	Itabn = 0;
Err*	Lasterr = (Err*)&err;
int	Lcolpos;
int	Len = LENGTH;
int	Line;
int	Linew = 0;
long	Lnumb = 0;
int	Margin = MARGIN;
int	Multi = 0;
int	Ncols = 1;
int	Nfiles = 0;
int	Nsepc = NSEPC;
int	Nspace;
char	nulls[] = "";
int	Numw;
int	Offset = 0;
int	Outpos;
int	Padodd;
int	Page;
int	Pcolpos;
int	Plength;
int	Sepc = 0;

extern	int	atoix(char**);
extern	void	balance(int);
extern	void	die(char*);
extern	void	errprint(void);
extern	char*	ffiler(char*);
extern	int	findopt(int, char**);
extern	int	get(int);
extern	void*	getspace(ulong);
extern	int	intopt(char**, int*);
extern	void	main(int, char**);
extern	Biobuf*	mustopen(char*, Fils*);
extern	void	nexbuf(void);
extern	int	pr(char*);
extern	void	put(long);
extern	void	putpage(void);
extern	void	putspace(void);

/*
 * return date file was last modified
 */
char*
getdate(void)
{
	static char *now = 0;
	static Dir *sbuf;
	ulong mtime;

	if(Nfiles > 1 || Files->f_name == nulls) {
		if(now == 0) {
			mtime = time(0);
			now = ctime(mtime);
		}
		return now;
	}
	mtime = 0;
	sbuf = dirstat(Files->f_name);
	if(sbuf){
		mtime = sbuf->mtime;
		free(sbuf);
	}
	return ctime(mtime);
}

char*
ffiler(char *s)
{
	return smprint("can't open %s\n", s);
}

void
main(int argc, char *argv[])
{
	Fils fstr[NFILES];
	int nfdone = 0;

	Binit(&bout, 1, OWRITE);
	Files = fstr;
	for(argc = findopt(argc, argv); argc > 0; --argc, ++argv)
		if(Multi == 'm') {
			if(Nfiles >= NFILES - 1)
				die("too many files");
			if(mustopen(*argv, &Files[Nfiles++]) == 0)
				nfdone++; /* suppress printing */
		} else {
			if(pr(*argv))
				Bterm(Files->f_f);
			nfdone++;
		}
	if(!nfdone)			/* no files named, use stdin */
		pr(nulls);		/* on GCOS, use current file, if any */
	errprint();			/* print accumulated error reports */
	exits(error? "error": 0);
}

int
findopt(int argc, char *argv[])
{
	char **eargv = argv;
	int eargc = 0, c;

	while(--argc > 0) {
		switch(c = **++argv) {
		case '-':
			if((c = *++*argv) == '\0')
				break;
		case '+':
			do {
				if(isdigit(c)) {
					--*argv;
					Ncols = atoix(argv);
				} else
				switch(c = TOLOWER(c)) {
				case '+':
					if((Fpage = atoix(argv)) < 1)
						Fpage = 1;
					continue;
				case 'd':
					Dblspace = 2;
					continue;
				case 'e':
					TABS(Etabn, Etabc);
					continue;
				case 'f':
					Formfeed++;
					continue;
				case 'h':
					if(--argc > 0)
						Head = argv[1];
					continue;
				case 'i':
					TABS(Itabn, Itabc);
					continue;
				case 'l':
					Len = atoix(argv);
					continue;
				case 'a':
				case 'm':
					Multi = c;
					continue;
				case 'o':
					Offset = atoix(argv);
					continue;
				case 's':
					if((Sepc = (*argv)[1]) != '\0')
						++*argv;
					else
						Sepc = '\t';
					continue;
				case 't':
					Margin = 0;
					continue;
				case 'w':
					Linew = atoix(argv);
					continue;
				case 'n':
					Lnumb++;
					if((Numw = intopt(argv, &Nsepc)) <= 0)
						Numw = NUMW;
				case 'b':
					Balance = 1;
					continue;
				case 'p':
					Padodd = 1;
					continue;
				default:
					die("bad option");
				}
			} while((c = *++*argv) != '\0');
			if(Head == argv[1])
				argv++;
			continue;
		}
		*eargv++ = *argv;
		eargc++;
	}
	if(Len == 0)
		Len = LENGTH;
	if(Len <= Margin)
		Margin = 0;
	Plength = Len - Margin/2;
	if(Multi == 'm')
		Ncols = eargc;
	switch(Ncols) {
	case 0:
		Ncols = 1;
	case 1:
		break;
	default:
		if(Etabn == 0)		/* respect explicit tab specification */
			Etabn = DEFTAB;
	}
	if(Linew == 0)
		Linew = Ncols != 1 && Sepc == 0? LINEW: 512;
	if(Lnumb)
		Linew -= Multi == 'm'? Numw: Numw*Ncols;
	if((Colw = (Linew - Ncols + 1)/Ncols) < 1)
		die("width too small");
	if(Ncols != 1 && Multi == 0) {
		ulong buflen = ((ulong)(Plength/Dblspace + 1))*(Linew+1)*sizeof(char);
		Buffer = getspace(buflen*sizeof(*Buffer));
		Bufend = &Buffer[buflen];
		Colpts = getspace((Ncols+1)*sizeof(*Colpts));
	}
	return eargc;
}

int
intopt(char *argv[], int *optp)
{
	int c;

	if((c = (*argv)[1]) != '\0' && !isdigit(c)) {
		*optp = c;
		(*argv)++;
	}
	c = atoix(argv);
	return c != 0? c: -1;
}

int
pr(char *name)
{
	char *date = 0, *head = 0;

	if(Multi != 'm' && mustopen(name, &Files[0]) == 0)
		return 0;
	if(Buffer)
		Bungetc(Files->f_f);
	if(Lnumb)
		Lnumb = 1;
	for(Page = 0;; putpage()) {
		if(C == -1)
			break;
		if(Buffer)
			nexbuf();
		Inpos = 0;
		if(get(0) == -1)
			break;
		Bflush(&bout);
		Page++;
		if(Page >= Fpage) {
			if(Margin == 0)
				continue;
			if(date == 0)
				date = getdate();
			if(head == 0)
				head = Head != 0 ? Head :
					Nfiles < 2? Files->f_name: nulls;
			Bprint(&bout, "\n\n");
			Nspace = Offset;
			putspace();
			Bprint(&bout, HEAD);
		}
	}
	if(Padodd && (Page&1) == 1) {
		Line = 0;
		if(Formfeed)
			put('\f');
		else
			while(Line < Len)
				put('\n');
	}
	C = '\0';
	return 1;
}

void
putpage(void)
{
	int colno;

	for(Line = Margin/2;; get(0)) {
		for(Nspace = Offset, colno = 0, Outpos = 0; C != '\f';) {
			if(Lnumb && C != -1 && (colno == 0 || Multi == 'a')) {
				if(Page >= Fpage) {
					putspace();
					Bprint(&bout, "%*ld", Numw, Buffer?
						Colpts[colno].c_lno++: Lnumb);
					Outpos += Numw;
					put(Nsepc);
				}
				Lnumb++;
			}
			for(Lcolpos=0, Pcolpos=0; C!='\n' && C!='\f' && C!=-1; get(colno))
					put(C);
			if(C==-1 || ++colno==Ncols || C=='\n' && get(colno)==-1)
				break;
			if(Sepc)
				put(Sepc);
			else
				if((Nspace += Colw - Lcolpos + 1) < 1)
					Nspace = 1;
		}
	/*
		if(C == -1) {
			if(Margin != 0)
				break;
			if(colno != 0)
				put('\n');
			return;
		}
	*/
		if(C == -1 && colno == 0) {
			if(Margin != 0)
				break;
			return;
		}
		if(C == '\f')
			break;
		put('\n');
		if(Dblspace == 2 && Line < Plength)
			put('\n');
		if(Line >= Plength)
			break;
	}
	if(Formfeed)
		put('\f');
	else
		while(Line < Len)
			put('\n');
}

void
nexbuf(void)
{
	Rune *s = Buffer;
	Colp p = Colpts;
	int j, c, bline = 0;

	for(;;) {
		p->c_ptr0 = p->c_ptr = s;
		if(p == &Colpts[Ncols])
			return;
		(p++)->c_lno = Lnumb + bline;
		for(j = (Len - Margin)/Dblspace; --j >= 0; bline++)
			for(Inpos = 0;;) {
				if((c = Bgetrune(Files->f_f)) == -1) {
					for(*s = -1; p <= &Colpts[Ncols]; p++)
						p->c_ptr0 = p->c_ptr = s;
					if(Balance)
						balance(bline);
					return;
				}
				if(ISPRINT(c))
					Inpos++;
				if(Inpos <= Colw || c == '\n') {
					*s = c;
					if(++s >= Bufend)
						die("page-buffer overflow");
				}
				if(c == '\n')
					break;
				switch(c) {
				case '\b':
					if(Inpos == 0)
						s--;
				case ESC:
					if(Inpos > 0)
						Inpos--;
				}
			}
	}
}

/*
 * line balancing for last page
 */
void
balance(int bline)
{
	Rune *s = Buffer;
	Colp p = Colpts;
	int colno = 0, j, c, l;

	c = bline % Ncols;
	l = (bline + Ncols - 1)/Ncols;
	bline = 0;
	do {
		for(j = 0; j < l; ++j)
			while(*s++ != '\n')
				;
		(++p)->c_lno = Lnumb + (bline += l);
		p->c_ptr0 = p->c_ptr = s;
		if(++colno == c)
			l--;
	} while(colno < Ncols - 1);
}

int
get(int colno)
{
	static int peekc = 0;
	Colp p;
	Fils *q;
	long c;

	if(peekc) {
		peekc = 0;
		c = Etabc;
	} else
	if(Buffer) {
		p = &Colpts[colno];
		if(p->c_ptr >= (p+1)->c_ptr0)
			c = -1;
		else
			if((c = *p->c_ptr) != -1)
				p->c_ptr++;
	} else
	if((c = (q = &Files[Multi == 'a'? 0: colno])->f_nextc) == -1) {
		for(q = &Files[Nfiles]; --q >= Files && q->f_nextc == -1;)
			;
		if(q >= Files)
			c = '\n';
	} else
		q->f_nextc = Bgetrune(q->f_f);
	if(Etabn != 0 && c == Etabc) {
		Inpos++;
		peekc = ETABS;
		c = ' ';
	} else
	if(ISPRINT(c))
		Inpos++;
	else
		switch(c) {
		case '\b':
		case ESC:
			if(Inpos > 0)
				Inpos--;
			break;
		case '\f':
			if(Ncols == 1)
				break;
			c = '\n';
		case '\n':
		case '\r':
			Inpos = 0;
		}
	return C = c;
}

void
put(long c)
{
	int move;

	switch(c) {
	case ' ':
		Nspace++;
		Lcolpos++;
		return;
	case '\b':
		if(Lcolpos == 0)
			return;
		if(Nspace > 0) {
			Nspace--;
			Lcolpos--;
			return;
		}
		if(Lcolpos > Pcolpos) {
			Lcolpos--;
			return;
		}
	case ESC:
		move = -1;
		break;
	case '\n':
		Line++;
	case '\r':
	case '\f':
		Pcolpos = 0;
		Lcolpos = 0;
		Nspace = 0;
		Outpos = 0;
	default:
		move = (ISPRINT(c) != 0);
	}
	if(Page < Fpage)
		return;
	if(Lcolpos > 0 || move > 0)
		Lcolpos += move;
	if(Lcolpos <= Colw) {
		putspace();
		Bputrune(&bout, c);
		Pcolpos = Lcolpos;
		Outpos += move;
	}
}

void
putspace(void)
{
	int nc;

	for(; Nspace > 0; Outpos += nc, Nspace -= nc)
		if(ITABS)
			Bputc(&bout, Itabc);
		else {
			nc = 1;
			Bputc(&bout, ' ');
		}
}

int
atoix(char **p)
{
	int n = 0, c;

	while(isdigit(c = *++*p))
		n = 10*n + c - '0';
	(*p)--;
	return n;
}

/*
 * Defer message about failure to open file to prevent messing up
 * alignment of page with tear perforations or form markers.
 * Treat empty file as special case and report as diagnostic.
 */
Biobuf*
mustopen(char *s, Fils *f)
{
	char *tmp;

	if(*s == '\0') {
		f->f_name = STDINNAME();
		f->f_f = malloc(sizeof(Biobuf));
		if(f->f_f == 0)
			cerror("no memory");
		Binit(f->f_f, 0, OREAD);
	} else
	if((f->f_f = Bopen(f->f_name = s, OREAD)) == 0) {
		tmp = ffiler(f->f_name);
		s = strcpy((char*)getspace(strlen(tmp) + 1), tmp);
		free(tmp);
	}
	if(f->f_f != 0) {
		if((f->f_nextc = Bgetrune(f->f_f)) >= 0 || Multi == 'm')
			return f->f_f;
		sprint(s = (char*)getspace(strlen(f->f_name) + 1 + EMPTY),
			"%s -- empty file\n", f->f_name);
		Bterm(f->f_f);
	}
	error = 1;
	cerror(s);
	fprint(2, "\n");
	return 0;
}

void*
getspace(ulong n)
{
	void *t;

	if((t = malloc(n)) == 0)
		die("out of space");
	return t;
}

void
die(char *s)
{
	error++;
	errprint();
	cerror(s);
	Bputc(&bout, '\n');
	exits("error");
}

/*
void
onintr(void)
{
	error++;
	errprint();
	exits("error");
}
/**/

/*
 * print accumulated error reports
 */
void
errprint(void)
{
	Bflush(&bout);
	for(; err != 0; err = err->e_nextp) {
		cerror(err->e_mess);
		fprint(2, "\n");
	}
}
