/*

multicol.c      lay out text in multiple columns

Copyright (c) 1998 by   Andreas Leitgeb (AvL) <avl@logic.tuwien.ac.at>

Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and that
both that copyright notice and this permission notice appear in
supporting documentation. 

*/

#include<stdio.h>
#include<stdlib.h>
#include<ctype.h>
#include<string.h>
#define MAXSTRLEN 1024

/* Integer-Options:
  cpl:  width of a block (chars_per_line)  
  lpb:  height of a block (lines_per_block)
  cpp:  number of columns (cols_per_page)
  tab:  tabsize
*/
int cpl=80,lpb=66,cpp=2,tab=8;

/* Flags-Options:
  overstrike:  keep "overstrike"-chars: '\b' and '\r'
               otherwise act like  "col -b"
  wrap:        wrap long lines (length > cpl)
               otherwise truncate
*/
int overstrike=1,wrap=1;

/* Box-framing-styles:
    1(none):  (if boxes are separated externally (e.g. physical pages))
    2(vert):  put a "\n--\n\n" between rows of boxes.
    3(box):   frame with  '+-|' textgraphics (default)
    more to come ...
*/
int boxstyle=3;

void No_Mem(), Buffer_Overflow(), finish_block(), finish_line();
void ProcessBlocks(), DestroyBlocks();

/* List-handling: */
struct ListElem {
	struct ListElem *next;
	void *data;
};
struct List {
	struct ListElem *head,*tail,*cur; int len;
};
struct Line {
	int num_chars,num_cols; char *str;
};

struct List *MkNewList() 
{ 
	struct List *tmp=malloc(sizeof(struct List)); 
	if (tmp) { tmp->head=tmp->tail=tmp->cur=NULL; tmp->len=0;} 
	else No_Mem();
	return tmp;
}

void *RmFirstElem(struct List *lst)
{
	struct ListElem *tmp=NULL; void *data=NULL;
	if (!lst || !lst->head) return NULL;
	tmp=lst->head; lst->head=lst->head->next; lst->len--;
	if (lst->head==NULL) { lst->tail=NULL; }
	data=tmp->data; tmp->data=NULL; tmp->next=NULL;
	free(tmp); return data;
}
void Append(struct List *lst,void *data)
{
	struct ListElem *tmp; if (!lst) return;
	tmp=malloc(sizeof(struct ListElem));
	if (!tmp) No_Mem();
	tmp->data=data; tmp->next=NULL; 

	if (lst->tail) { /* non-empty list */
		lst->tail->next=tmp; lst->tail=tmp;
	} else { /* empty list */
		lst->tail=lst->head=tmp;
	}
	lst->len++;
}
struct Line *MkLine(char *str,int num_chars,int num_cols) 
{
	struct Line *tmp; tmp=malloc(sizeof(struct Line));
	if (tmp) { char *tmp_str;
		tmp_str=malloc(num_chars);
		if (!tmp_str) { free(tmp); No_Mem(); }
		memcpy(tmp_str,str,num_chars); tmp->str=tmp_str;
		tmp->num_chars=num_chars; tmp->num_cols=num_cols;
	} else No_Mem();
	return tmp;
}

int curpos=0,strpos=0,firstrow=1,lastrow=0; 
char str[MAXSTRLEN],*cmd; int ch;
struct List *ListOfLines=NULL, *ListOfBlocks=NULL;

void Buffer_Overflow() {
	fprintf(stderr,"Line too long\n");
	exit(2);
}
void No_Mem() {
	fprintf(stderr,"malloc failed\n");
	exit(2);
}

void finish_line() {
	struct Line *line=MkLine(str,strpos,curpos);
	strpos=0; curpos=0;  
	
	if (ListOfLines->len >= lpb) {
		finish_block();
	} 
	Append(ListOfLines,line);
}
void ProcessBlocks() {
	struct List *block; char collstr[(MAXSTRLEN+cpl+4)*cpp+1]; int collpos;
	int l; char dummych; struct Line *dummy=MkLine(&dummych,0,0); 
	for(ListOfBlocks->cur=ListOfBlocks->head;
	    ListOfBlocks->cur != NULL;
	    ListOfBlocks->cur=ListOfBlocks->cur->next)
	{
		block=(struct List*)ListOfBlocks->cur->data;
		block->cur=block->head;
	}
	if (firstrow && boxstyle==3) { 
		collstr[0]='+'; collpos=1;
		for(l=0; l< ListOfBlocks->len ; l++) { 
			memset(collstr+collpos,'-',cpl+2); collpos+= cpl+2;
			collstr[collpos]='+'; collpos++;
		}
		fwrite(collstr,collpos,1,stdout); fputs("\n",stdout);
	}
	if (!firstrow && boxstyle==2) {
		fputs("\n--\n\n",stdout);
	}
	for (l=1; l<=lpb; l++) {
		collpos=0; 
		if (boxstyle==3) { collstr[collpos]='|'; collpos++; }
		for(ListOfBlocks->cur=ListOfBlocks->head;
		    ListOfBlocks->cur != NULL;
		    ListOfBlocks->cur=ListOfBlocks->cur->next)
		{
			struct Line *line; int fillspc;
			block=(struct List*)ListOfBlocks->cur->data;
			if (block->cur) { 
				line=block->cur->data;
				block->cur=block->cur->next; 
			} else { 
				line=dummy;
			}
			if (boxstyle==3) { collstr[collpos]=' '; collpos++; }
			memcpy(collstr+collpos,line->str,line->num_chars);
			collpos += line->num_chars; 
			fillspc = cpl - line->num_cols;
			if (fillspc > 0) {
				memset(collstr+collpos,' ',fillspc);
				collpos += fillspc;
			}
			if (boxstyle==3) {
				collstr[collpos]=' '; collpos++;
				collstr[collpos]='|'; collpos++;
			}
		}
		fwrite(collstr,collpos,1,stdout); fputs("\n",stdout);
	}
	if (boxstyle==3) { 
		collstr[0]='+'; collpos=1; 
		for(l=0; l< ListOfBlocks->len ;l++)
		{
			memset(collstr+collpos,'-',cpl+2); collpos+= cpl+2;
			collstr[collpos]='+'; collpos++;
		}
		fwrite(collstr,collpos,1,stdout); fputs("\n",stdout);
	}
	firstrow=0; free (dummy->str); free (dummy);
	fflush(stdout);
}
void DestroyBlocks() {
	struct List *block;
	while ((block=RmFirstElem(ListOfBlocks)) != NULL) {
		struct Line *line;
		while ((line=RmFirstElem(block)) != NULL) {
			free (line->str); free (line);
		}
		free (block);
	}
}
void finish_block() {
	if (ListOfLines->len >0) {
		Append(ListOfBlocks,ListOfLines);
		ListOfLines=MkNewList();
	}
	if (ListOfBlocks->len >= cpp || lastrow) {
		ProcessBlocks();
		DestroyBlocks();
	}
}

int main(int argc,char *argv[]) 
{ 
   int i; char *infname=NULL,*outfname=NULL; char *cptr;
   cmd=argv[0];

   ListOfLines=MkNewList(); ListOfBlocks=MkNewList();

   for (i=1 ; i<argc; ++i) { 
      cptr=argv[i];
      if (cptr[0]!='-') {
         if (infname) {
            fprintf(stderr,"%s: More than one %sputfile given.\n",argv[0],"in");
            exit(2);
         }
         infname=cptr;
      } else {
         switch (cptr[1]) {
            case 'i': 
               if (cptr[2]) { cptr+=2; }
               else { cptr=argv[i+1]; i++; }
               if (!cptr || !*cptr) {
                  fprintf(stderr,"%s: Argument -%c requires a filename given.\n",argv[0],'i');
                  exit(2);
               }
               if (infname) {
                  fprintf(stderr,"%s: More than one %sputfile given.\n",argv[0],"in");
                  exit(2);
               }
               infname=cptr; i++; break;
            case 'o': 
               if (cptr[2]) { cptr+=2; }
               else { cptr=argv[i+1]; i++; }
               if (!cptr || !*cptr) {
                  fprintf(stderr,"%s: Argument -%c requires a filename given.\n",argv[0],'o');
                  exit(2);
               }
               if (outfname) {
                  fprintf(stderr,"%s: More than one %sputfile given.\n",argv[0],"out");
                  exit(2);
               }
               outfname=cptr; i++; break;
               case 'n': 
                  if (cptr[2]!=0) {cptr+=2;} 
                  else { cptr=argv[i+1];i++; } 
                  if (!cptr) { cptr=""; }
                  cpp=atoi(cptr); if (cpp<1) goto usage;
                  break;
               case 'h': 
                  if (cptr[2]!=0) {cptr+=2;} 
                  else { cptr=argv[i+1];i++; } 
                  if (!cptr) { cptr=""; }
                  lpb=atoi(cptr); if (lpb<1) goto usage;
                  break;
               case 'w': 
                  if (cptr[2]!=0) {cptr+=2;} 
                  else { cptr=argv[i+1];i++; } 
                  if (!cptr) { cptr=""; }
                  cpl=atoi(cptr); if (cpl<1) goto usage;
                  break;
               case 't': 
                  if (cptr[2]!=0) {cptr+=2;} 
                  else { cptr=argv[i+1];i++; } 
                  if (!cptr) { cptr=""; }
                  tab=atoi(cptr); if (tab<1) goto usage;
                  break;
               case 'b': 
                  if (cptr[2]!=0) {cptr+=2;} 
                  else { cptr=argv[i+1];i++; } 
                  if (!cptr) { cptr=""; }
                  boxstyle=atoi(cptr); 
                  if (boxstyle<1 || boxstyle>3) goto usage;
                  break;
           default:
           usage:
               fprintf(stderr,
"Usage: %s <option> [[-i] inputfile] [-o outfile]\n"
"  if in/outfile are omitted, stdin/stdout are used instead\n"
"  options:  -w#  (chars per line)  -h# (lines per block)\n"
"            -t#  (tabsize)         -n# (blocks per row)\n"
"            -b{1,2,3} boxstyle: 1:none  2:row-separators  3:framed boxes\n"
,argv[0]);
               exit(2);
         }
      }
   }
   if (infname && !freopen(infname,"r",stdin)) {
      fprintf(stderr,"%s: Cannot open %s for reading.\n",argv[0],infname);
      exit(1);
   }
   if (outfname && !freopen(outfname,"wb",stdout)) {
      fprintf(stderr,"%s: Cannot open %s for writing.\n",argv[0],outfname);
      exit(1);
   }


/* 
   allocate separate buffers for each line in each block, and
   only merge them before output. Each line can get (almost) 
   arbitrary long (due to ^H's) !
*/
   /* int curpos,strpos; */
   curpos=0; strpos=0;

   while ((ch=fgetc(stdin))!=EOF) {
      switch(ch) {
      case '\b': /* BackSpace */
         if (overstrike) {
            if (curpos>0) {
               str[strpos]='\b'; curpos--; strpos++; 
               if (strpos>=MAXSTRLEN) { Buffer_Overflow(); }
            } /* else ignore */
         } else { /* non-overstrike */
            if (curpos>0) {
               curpos--; strpos--; 
            } /* else ignore */
         }   
         break;
      case '\n': /* NewLine */
         finish_line();
         break;
      case '\f': /* FormFeed */
         if (strpos>0) { finish_line(); }  
         finish_block();
         break;
      case '\t': /* Tabulator */
         if (curpos>=cpl) { finish_line(); }
         {  int newpos=curpos + tab - (curpos % tab);
            if (newpos>=cpl) { finish_line(); }
            else {
               memset(str+strpos, ' ', newpos-curpos);
               strpos+=newpos-curpos; curpos=newpos;
            }
         }
         break;
      case 0: /* Null */
         break;
      case '\r': /* CarriageReturn */
         break;

      default:
         /* some printable char, that consumes (at least) one column */
         if (curpos>=cpl) { finish_line(); }
         str[strpos]=ch; curpos++; strpos++;
         if (strpos>=MAXSTRLEN) { Buffer_Overflow(); }
      }
   }
   if (ch==EOF) { 
      lastrow=1;
      if (strpos>0) { finish_line(); }  
      finish_block();
   }

   free (ListOfBlocks); free (ListOfLines);

   return(0);
}


