#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);
       Blethal(&bout, nil);
       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);
               Blethal(f->f_f, nil);
       } 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) {
               Blethal(f->f_f, nil);
               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");
       }
}