/*++++++++++++++
 Copyright:    (C) 2008-2015 UDS/CNRS
 License:       GNU General Public License
.IDENTIFICATION reformat.c
.LANGUAGE       C
.AUTHOR         Francois Ochsenbein [CDS]
.ENVIRONMENT    Unix
.KEYWORDS       Translate
.VERSION  1.0   26-Feb-1995
.VERSION  1.1   28-Feb-1995: Added -c option
.VERSION  1.2   14-Mar-1995: Removed bug in exec()
.VERSION  1.3   28-Apr-1995: Another bug in exec...
.VERSION  1.4   18-Sep-1995: Option -d
.VERSION  1.5   28-May-1996: Also -j
.VERSION  1.6   16-Jan-1999: -W and -c options
.VERSION  1.7   21-Mar-2001: Problem with 8-bit letters...
.VERSION  1.8   12-Jan-2003: -p option
.VERSION  1.9   07-Sep-2004: Accept stop in between angle brackets in ><
.VERSION  1.91  24-Nov-2007: Keep lines made of == or --
.VERSION  1.92  05-Dec-2007: -d option: first line indented if starts by blank
.VERSION  1.93  03-May-2008: -c option bug!
.VERSION  1.94  19-Jan-2012: -c option bug!
.VERSION  1.95  27-Mar-2013: Accept option in a single -
.COMMENTS       Insert newlines, remove \r, adjusrt without cutting words
---------------*/

#define VERSION "1.95 (37-Mar-2013)"
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <strings.h>

#define MAX(x,y)        ((x) >= (y) ? (x) : (y))
#define ITEMS(a)        sizeof(a)/sizeof(a[0])

static int verbop   = 0;  /* Verbose option       */
static int indent   = 0;
static int width    = 80;
static FILE *input  = (FILE *)0;
static int errors   = 0;  /* Errors counter       */
static char ocont;	  /* 1=-c; 2=-d		  */
static char ojust;	  /* Justify		  */
static char opar ;	  /* Paragraph mode 	  */
static char oupper ;	  /* Uppercase 'I' 'W'	  */
static char linesep;	  /* Flag used by newline */
static char *blist;	  /* -b option	          */

static char *aline, *eline, *cline, *bline ;
static char *aleft ;	  /* In cunjunction -f    */

static char pgm[] = "*** reformat";
static char usage[] = "\
Usage: reformat [-[c|d|f|j|p]] [-b[sep]] [-[I|i] indent] [-[W|w] width]\n\
       [file]...\n\
" ;
static char help[] = "\
   -b: break lines for characters followed by newline, default list is :)\n\
   -c: only continuation lines are indented\n\
   -d: lines starting by non-blank are left-adjusted\n\
   -f: fill the indentation with contents of previous line\n\
       (useful with the -I option)\n\
   -i: specifies the indentation (default 0)\n\
   -I: the indentation represents an \"identification\" column\n\
   -j: asks for Justification (align left and right margins)\n\
   -p: one item is made of a paragraph\n\
   -w: specifies the width of the text\n\
   -W: specifies the linesize = width+indentation\n\
 Lines starting by a punctuation or with a different indentation\n\
      are assumed to start a new paragraph\n\
" ;

/*==================================================================
		Internal Routines
 *==================================================================*/
static void error(text)
/*++++++++++++++++
.PURPOSE  Error exit
.RETURNS  -
-----------------*/
  char *text;     /* IN: Text before error */
{
    if (text)   fprintf(stderr, "%s: %s\n", pgm, text);
    fprintf(stderr, "%s\n", usage);
    exit(1);
}

int justify (str, len, size)
/*+++
.PURPOSE Justify (left & right) the string 
.RETURNS 0
.REMARKS 
---*/
  char 	*str;		/* IN: string to justify	*/
  int 	len;		/* IN: Length of str		*/
  int   size;		/* IN: Size of str (line width) */
{
  static char blank = ' ';
  register char	*p1, *p2;
  register int ls;
  int	 sb, rb, pb, vb, i, nb;

    /* Fill the line with blanks */
    while(len < size) str[len++] = blank ;

    /* First, start at first non-blank char */
    while ( (*str == blank) && (len > 0))	  { str++; len--; }
  
    /* ... and count trailing blanks. 
     *	This gives the length ls of the source string 	*/

    for (p1 = str + len, nb = 0; (--p1 >= str) && (*p1 == blank); nb++)	;
    if (nb == 0) return(0);
    ls = len - nb;

	/* Now, count existing blanks in the string	*/

    nb = 0;
    while (--p1 >= str) if (*p1 == blank)  nb++;

    if (nb == 0)	return(0);	/* Can't be justified...	*/


    sb = (len - ls)/nb;			/* Systematic blanks		*/
    rb = (len - ls)%nb;			/* Random blanks		*/

    /* Can we assign the random blanks to comma follow-up ? */
    for (p1=str+ls, vb=0; (vb < rb) && (--p1>str) ; )	{
	if ((p1[0] == blank) && ispunct(p1[-1])) vb++ ;
    }
    rb -= vb ;				/* Random blanks now diminished	*/

    pb = 0;				/* Periodic blanks		*/
    if (rb) {
	pb = nb / rb;
    }

    for (nb=0, p1=str+ls, p2=str+len; --p1>=str; ) {
    	*(--p2) = *p1;
	if (*p1 == blank) {
	    nb++;
	    /* Add the Systematic Blanks */
	    for (i=sb; --i>=0; )	*(--p2) = blank;
	    if ((vb>0) && ispunct(p1[-1])) 	/* Blank after a punct. */
		{ *(--p2) = blank; vb--; }
	    if (rb>0) 				/* Add a random blank.	*/
		{ if (nb%pb == 0) { *(--p2) = blank; rb--; } }
	}
    }
    return(0);
}

static void newline(indent)
/*++++++++++++++++
.PURPOSE  Write a newline + indentation
.RETURNS  ---
-----------------*/
  int indent;	/* IN: Number of blanks for newline */
{
  char *p ; int i;
    for (p = cline ; (p > aline) && (p[-1] == ' ') ; p--)  ;
    if (p > aline) fwrite(aline, 1, p-aline, stdout), linesep = '\n';
    if (linesep) putchar(linesep);
    linesep = '\n';
    /* Copy the left part as blanks or aleft */
    p = aleft ? aleft : (char *)0;
    for (cline=aline, i=indent; (--i >= 0) && (cline < eline); cline++) {
	if (p && *p) *cline = *(p++) ;
	else *cline = ' ' ;
    }
    bline = cline ;
    fflush(stdout) ;
}

static int exec()
/*++++++++++++++++
.PURPOSE  Execute the transformation
.RETURNS  0 = OK 
-----------------*/
{
  static int init_done ;
  int  cc, pc, newpar;
  int  b, a;		/* b = current indentation, a = previous indent */
  int  nbb; 		/* non-blank bytes */
  char *aword, *eword, *cword, *aident, *eident;
  char *p ;
  
    if (!init_done) {
        if (oupper&2) {	/* 'W' option */
	    width -= indent ;
	    if (width < 20) width += indent, fprintf(stderr, 
	    "++++Width -W %d too small, changed to -w %d\n", width, width) ;
        }
	init_done = 1 ;
    }

    aline = cline = malloc(2*width + indent + MAX(indent, width) + 4); 
    eline = aline + width + indent ;
    aword = eline; eword = aword + width ;
    if (oupper&1) {
	aident = aword + MAX(indent, width) + 2 ;
	eident = aword + indent ;
	memset(aident, ' ', indent) ;
	aident[indent] = 0 ;
    }
    else aident = eident = (char *)0 ;
    if (aleft) aleft = aident ;			/* To Fill left part */

    linesep = 0;

    /* Initial indentation:
     * - in -c mode, flush left;
     * - in -d mode, flush left if line first line does not start by a blank
     * - other modes: insert the indentation
     **/
    if (ocont&2) {		/* -d mode, V1.92 */
	cc = fgetc(input);
	if ((cc == ' ') || (cc == '\t'))
	     b = indent;
	else b = 0;
	ungetc(cc, input);
    }
    else if (ocont&1)		/* -c mode, V1.92 */
	 b = 0;
    else b = indent;
    newline(b);			/* Initial identation */
    linesep = 0;

    a = -1; b = 0;
    newpar = 1;
    for (pc=cc='\n'; pc != EOF; pc = cc) {
    	if (aident && (pc == '\n')) {			/* -I: Check   */
	    nbb=0;
	    if (opar && (!newpar)) goto nextWord; /* Continuation paragraph */
	    else for (cword=aword; cword<eident; cword++) {
		*cword = cc = fgetc(input);
		if (cc == EOF) break ;
		if (cc == '\n') break ;
		if (cc != ' ') nbb++ ;
	    }
	    *cword = 0 ;
	    if (nbb && strcmp(aword, aident)) {		/* New ident	*/
		newline(0) ;
		strcpy(aident, aword) ;
		strcpy(aline, aword) ; 
		cline = aline + strlen(aline) - 1;
		if (*cline != ' ') cline++ ;
	    }
	    continue ;
	}
#if 0
	/* First Word of line: is it adjusted ? */
	if ((ocont&2) && (pc == '\n')) {
	    cc = fgetc(input);
	    if (isspace(cc)) ; else newline(0);
	    ungetc(cc, input);
	}
#endif
      nextWord:
        for (cword=aword; cword<eword; cword++) {	/* Read a word */
	    *cword = cc = fgetc(input);
            if (cc == EOF) break;
            if (isspace(cc)) break;
    	}
	if (pc == '\n') {
	    b=0;
	    if (ocont == 1) { 	/* Start by indentation keep it ! */
		if (isspace(*aword)) {
		    if (cc == '\t') b = 8;
		    else if (cc == ' ') b=1;
		}
	    }
	    else if ((cword == aword)) {		
	        /* New line: count this identication (not in -c option) */
	        while (isspace(cc)) {
	            if (cc == '\n') {	/* A blank line */
		        if(cline > bline) {
		            /* In paragraph mode, we keep empty lines 
		             * as separators */
		            newline(opar || ocont ? 0 : indent);
		        }
		        else if (ocont != 2) newline(indent);
		        b = 0;
	                newpar = 1;
	            }
	            else { 			/* Count indentation blanks */
	                b++; 
	                if (cc == '\t') b++;
		    }
		    pc = cc;
		    cc = fgetc(input);
	        }
	        if (cc == EOF) break;
	        ungetc(cc, input);
	        cc = pc;
	        continue ;
	    }
	}

	/* V1.91: a rule is on a new line */
	p = (char *)0;		/* Converted to non-zero if a rule */
	if ((pc == '\n') && ispunct(*aword) && ((cword-aword)>=8)) {
	    for (p=aword; (p < cword) && (*p == *aword); p++) ;
	    if (p == cword) ;
	    else if (((cword-p) == 1) && (cc == '\n')) ;
	    else p = (char *)0;
	}
	if (p) {		/* A rule was encountered ! */
	    if(cline > bline) {	/* Write current line */
		newline(b);
	    }
	    for (p=aword; p<cword; p++) *(cline++) = *p;
	    if (cc == *aword) {	/* Rule across a long line  */
		fwrite(aline, 1, cline-aline, stdout);
		cline = bline = aline;
	    }
	    continue;
	}
	
	newpar = 0;
	if (pc == '\n') {			/* First Word in line	*/

	    /* In -c option, newlines are kept with no indentation
				 (indentation only for continuation lines) */
	    if (ocont == 1) newline(b);
	    
	    /* In -d option, it's a continuation ONLY if first char
		   is blank...
	    */
	    else if (ocont == 2) { if(b == 0) newline(0); }

    	    else if (cword > aword) {		/* A word to write 	*/
		/* Lines starting with % or .  are left-flushed		*/
    	    	if ( (b == 0) && ((*aword == '%')||(*aword == '.'))) {
			newline(0);
	        }
    	    	else {			/* More indentation	*/
    	    	    /* printf("(parg)a=%d, b=%d", a, b); */
    	    	    if (b>a) newline(indent);
    	    	    a=b;
    	    	}
		b=0;
	    }
    	}

	if (cword == aword) /* Only a blank */ {
	    if (ocont == 0) continue ;
	    if (cline >= eline) continue; 	/* Ignore ending blanks  */
	    if (cline > bline) *(cline++) = ' ';
	    continue ;
	}

    	if ((cline + (cword-aword)) >= eline) { 	/* Too long line */
    	    if(ojust) justify(bline, cline-bline, eline-bline), cline=eline;
	    newline(indent) ;
	}
    	if (cline > bline) *(cline++) = ' ';
	for(p=aword; p<cword; p++) *(cline++) = *p ;

	if (cc == '\n') {
	    if (blist && (cline>aline) && strchr(blist, cline[-1])) 
		newline(ocont==1 ? 0 : indent);
	    b = 0 ;
	}
    }

    newline(0); fflush(stdout);
    free(aline); aline = eline = cline = (char *)0;
    return(0);
}

/*==================================================================*/

#define pVal(text)      if (! *++p) {                           \
				if (--argc <= 0) error(text);   \
				else    p = *++argv;            \
			}
#define error_exclusive(o) { fprintf(stderr, \
	"#***options %s are exclusive!\n%s\n", o, usage); \
        exit(1); }
/*==================================================================
		Program
 *==================================================================*/
int main (argc, argv)
int argc; char **argv;
{
  char *p;

	/* Check the Arguments */
  
  while (-- argc > 0) {
	p = *++argv;
	if (*p == '-') for (++p; *p; p++) switch(*p) {
	  case 'b':
	        blist = p[1] ? p+1 : ":)";
		p += strlen(p)-1;
		continue;
	  case 'c':		/* Cont. Only */
		if (ocont|opar) error_exclusive("c,d,p");
		ocont = 1;
		continue;
	  case 'd':		/* Data Def   */
		if (ocont|opar) error_exclusive("c,d,p");
		ocont = 2;
		continue;
	  case 'I':
		pVal("Missing -i argument");
		indent = atoi(p);
		oupper |= 1 ;
		p += strlen(p)-1;
		continue;
	  case 'f':		/* Fill   */
		aleft = (char *)1 ;
		continue ;
	  case 'i' :            /* Indent */
		pVal("Missing -i argument");
		indent = atoi(p);
		p += strlen(p)-1;
		continue;
	  case 'j':		/* Justify  */
		ojust = 1;
		continue;
	  case 'p':		/* Justify  */
		if (ocont|opar) error_exclusive("c,d,p");
		opar  = 1;
		continue;
	  case 'W' :            /* Width   */
		pVal("Missing -W argument");
		width = atoi(p);
		oupper |= 2 ;
		p += strlen(p)-1;
		continue;
	  case 'w' :            /* Width   */
		pVal("Missing -w argument");
		width = atoi(p);
		p += strlen(p)-1;
		continue;
	  case 'v':                     /* Verbose      */
		if (strncmp(*argv, "-ver", 4) == 0) {
		    printf("reformat (CDS) Version %s\n", VERSION);
		    exit(0);
		}
		verbop = 1;
		/* fprintf(stderr, "... %s: Examine argument ", pgm+4); */
		continue;
	  case 'h':                     /* Help         */
		printf("%s%s", usage, help) ;
		exit(0);
	  default:
	  /* bad_option: */
		fprintf(stderr, "\n*** %s: Bad option: -%s\n", pgm+4, p);
		errors ++;
		exit (1);
	} else {
	    if (*p == '<') { 
		pVal("Input file");
	    }
	    input = fopen(p, "r");
	    if (! input) { perror(p); exit(1); }
	    if (exec()) exit (1);
	    fclose(input);
	}
    }
    if (! input) {
  	input = stdin;
  	if (exec()) exit(1);
    }
    exit(0);
}
