/*************************************************************************\
  wordwrap.c:
    Reformat a text on the input stream to a given maximum line length.
\*************************************************************************/

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>

char version[] = "$VER: wordwrap 2.2 (18.03.98)";

#define WORDLEN 200   /* max. size of an input word */
#define KEYLEN 40     /* max. size of keywords (for -e/-aA/-zZ) */
#define NDELIM 20     /* max. number of delimiter words */
typedef unsigned char UBYTE;   /* will make isspace() etc. work better */


/* lookup tables: */
int is_cons[256], is_alnum[256], is_lower[256];

/* adjustable parameters: */
UBYTE escword[KEYLEN];
UBYTE solword[NDELIM][KEYLEN], eolword[NDELIM][KEYLEN];
int scmplen[NDELIM], ecmplen[NDELIM];
int eolwords = 0, solwords = 0;
int lmax = 75, shortchars = 0, indent = 0, blank2i = 0, tabsize = 4;
unsigned int imargin = 0, omargin = 0;

/* switches: */
int blanks = 0, i2blank = 0, smart_b2i = 0;
int widespace = 0, unhyphen = 0, keepescapes = 0;


void init_tabs();
void wordwrap();
int getword(UBYTE s[], int lim);
int scrollmode(UBYTE *stop_at, int imrg, int omrg);


void help( UBYTE *s )
/* print a help text and nag about illegal parameter <s> */
    {
    if( s )
        fprintf( stderr, "illegal option '%s'\n", s );
    fprintf( stderr, "'wordwrap' command line parameters:\n");
    fprintf( stderr, " -l<len>                line length, defaults to 75\n");
    fprintf( stderr, " -s<len>                protect lines shorter than <len>\n");
    fprintf( stderr, " -b / -bc[<indent>]     protect blank lines / convert to indentation\n");
    fprintf( stderr, " -bC[<indent>]          like -bc, but convert single blank lines only\n");
    fprintf( stderr, " -i / -i<indent> / -ic  protect indentation / convert to blank lines\n");
    fprintf( stderr, " -ia / -ia<indent>      add blank lines before indentations\n");
    fprintf( stderr, " -t<tabsize>            how to expand tab indentations\n");
    fprintf( stderr, " -w / -W                wide spaces after '.!?' (and ':')\n");
    fprintf( stderr, " -m<width> / -M<width>  add / strip left margin\n");
    fprintf( stderr, " -a<sstr> / -A<sword>   substring / word always starting a new line\n");
    fprintf( stderr, " -z<estr> / -Z<eword>   substring / word always ending a line\n");
    fprintf( stderr, " -e<escw> / -E<escw>    (non)discardable 'escape words' protect sections\n");
    fprintf( stderr, " -h / -H                undo hyphenation (preserving hyphens)\n");
    }


int main( int argc, UBYTE* argv[] )
/* command line parsing */
    {
    UBYTE *s;

    init_tabs();
    *escword = '\0';
    while( --argc )
        {
        s = *++argv;
        if( *s++ == '-' )
            {
            switch( *s++ )
                {
                case 'l':
                    lmax = atoi( s );
                    break;
                case 's':
                    shortchars = atoi( s );
                    break;
                case 'h':
                    unhyphen = 1;
                    break;
                case 'H':
                    unhyphen = 2;
                    break;
                case 'w':
                    widespace = 1;
                    break;
                case 'W':
                    widespace = 2;
                    break;
                case 'm':
                    omargin = atoi(s);
                    break;
                case 'M':
                    imargin = atoi(s);
                    break;
                case 't':
                    tabsize = atoi(s);
                    break;
                case 'E':
                    keepescapes = 1;
                    /* drop-through! */
                case 'e':
                    strncpy( escword, s, KEYLEN-1 );
                    break;
                case 'b':
                    blanks = 1;
                    if( *s == 'c' || (smart_b2i = *s == 'C'))
                        {
                        blank2i = -1;   /* will be replaced by a value >0 later */
                        if( isdigit( *++s ) )
                            blank2i = atoi( s );
                        }
                    break;
                case 'i':
                    indent = -1;        /* indent by original width */
                    if( *s == 'c' )
                        {
                        indent = 0;     /* don't indent */
                        i2blank = 1;
                        }
                    if( *s == 'a' )
                        {
                        i2blank = 1;
                        s++;
                        }
                    if( isdigit( *s ) )
                        indent = atoi( s );     /* forced indentation width */
                    break;
                case 'a': case 'A':
                    if( solwords<NDELIM )
                        {
                        strncpy( solword[solwords], s, KEYLEN-1 );
                        scmplen[ solwords++ ] = islower( s[-1] ) ? strlen( s ) : 0;
                        }
                    else
                        {
                        fprintf( stderr, "too many -a/-A's\n" );
                        return 10;
                        }
                    break;
                case 'z': case 'Z':
                    if( eolwords<NDELIM )
                        {
                        strncpy( eolword[eolwords], s, KEYLEN-1 );
                        ecmplen[ eolwords++ ] = islower( s[-1] ) ? strlen( s ) : 0;
                        }
                    else
                        {
                        fprintf( stderr, "too many -z/-Z's\n" );
                        return 10;
                        }
                    break;
                default:
                    help(argv[0]);
                    return 10;

                }
            }
        else
            {
            help( argv[0] );
            return 10;
            }
        }
    if( blank2i<0 )
        {                       /* -bc without an explicit width */
        if( indent>0 )
            blank2i = indent;
        else
            blank2i = 4;
        }
    wordwrap();
    return 0;
    }


int lline;                      /* length of the current output line */
UBYTE joinme;                   /* the last character of a hyphenated word */

void newline()
/* little assistant to wordwrap(), not completely trivial */
    {
    int i;

    if( joinme )
        {
        putchar( '-' );         /* flush pending hyphen */
        joinme = 0;
        }
    putchar( '\n' );            /* perform line feed */
    lline = 0;
    for( i=1; i<=omargin; i++ )
        putchar( ' ' );         /* print left margin */
    }


void wordwrap()
/* central function, copies from <stdin> to <stdout> */
    {
    int i, lword, dented, breakme;
    UBYTE *s, c, word[WORDLEN];

    for( i=1; i<=omargin; i++ )
        putchar(' ');           /* left margin for the first output line */
    /* let's go */
    lline = 0;
    dented = 0;
    joinme = 0;
    breakme = 0;
    while( (lword = getword( word, WORDLEN )) )
        {
        breakme = (word[ lword-1 ] == '\n');
        if( breakme )           /* "short line" break request? */
            word[ --lword ] = '\0';
        /* four main cases: */
        /* 1) this was a blank line */
        if( lword == 0 )
            {
            if( blank2i )
                {
                if( lline )
                    newline();
                if( !(smart_b2i && dented) )
                    while( lline++ < blank2i )
                        putchar(' ');
                dented = 1;
                }
            else if( blanks )
                {
                if( lline )
                    newline();
                newline();
                }
            }
        /* 2) this was the start of an indented line */
        else if( isspace( word[0] ) )
            {
            if( (indent || i2blank) && lword>imargin )
                {
                if( lline )
                    newline();
                dented = 1;
                if( i2blank )
                    newline();
                if( indent>0 )
                    for( lline = 0; lline<indent; lline++ )
                        putchar(' ');
                else if( indent )
                    {
                    printf( "%s", &word[ imargin ] );
                    lline = lword-imargin;
                    }
                }
            }
        /* 3) we've read the escape word */
        else if( strcmp( word, escword ) == 0 )
            {
            if( keepescapes )
                {
                if( lline )
                    {
                    putchar(' ');
                    lline++;
                    }
                printf("%s", word);
                lline += lword;
                }
            if( lline )
                newline();
            lline = scrollmode( escword, imargin, omargin );
            }
        /* 4) a regular word */
        else
            {
            /* however, it might still be a "delimiter" word: */
            /* ... one that always ends a line? */
            for( i=0; i<eolwords; i++ )
                {
                s = word;
                if( ecmplen[i] && lword > ecmplen[i] )
                    s += lword-ecmplen[i];
                if( strcmp( s, eolword[i] ) == 0 )
                    breakme = 1;    /* treat it like a "short line" */
                }
            /* ... or one that must be at the start of a line? */
            for( i=0; i<solwords && lline; i++ )
                if( (scmplen[i] && strncmp( word, solword[i], scmplen[i] ) == 0 )
                  || strcmp( word, solword[i] ) == 0 )
                    newline();
            /* and we still might have to process the "hyphen" marker: */
            if( (c = word[ lword-1 ]) == '\t' )
                word[ --lword ] = '\0';
            /* now, finally, print the word: */
            if( lline == 0 || dented )
                {               /* at the start of the line */
                printf("%s", word);
                lline += lword;
                dented = 0;
                }
            else
                {               /* (attempt to) append to an existing line */
                if( unhyphen<2 && is_lower[ joinme ] && is_lower[ word[0] ]
                  && lline+lword <= lmax )
                    ;           /* this will join a previously hyphenated word */
                else if( joinme && is_alnum[word[0]] && lline+lword < lmax )
                    {           /* join a word, preserving the hyphen */
                    putchar('-');
                    lline++;
                    }
                else
                    {
                    if( joinme )
                        {
                        putchar('-');
                        lline++;
                        joinme = 0;
                        }
                    if( lline+lword < lmax )
                        {       /* standard case: insert a space between words */
                        putchar(' ');
                        lline++;
                        }
                    else        /* or start a new line */
                        newline();
                    }
                printf("%s", word);
                lline += lword;
                }
            /* hyphen marker processing, part 2: */
            if( c=='\t' )
                joinme = word[ lword-1 ];
            else
                joinme = 0;
            /* pending "short line" break? */
            if( breakme )
                newline();
            }
        }
        if( lline )
            {
            if( joinme )
                putchar( '-' );
            putchar( '\n' );
            }
    }


int getword( UBYTE s[], int lim )
/* Copies one word of the input stream to s[] (return value is strlen(s)),
 * then skips all subsequent spaces, stopping at the next word or EOL.
 * - will return "\n" for an empty line
 * - will return a string of blanks for a line starting with blanks
 * - will append " " to a word ending a sentence (if widespace != 0)
 * - will replace a trailing hyphen by "\t", (if at EOL and unhyphen != 0)
 * - will append "\n" to the last word on a "short" line
 * - will return an empty string at EOF
 * Note that the combinations " \n" and "\t\n" may very well occur,
 * whereas combinations of " " and "\t" won't.
 */
    {
    int c, j, i = 0;
    static int nonblanks = 0;

    c = getchar();
    if( c == EOF )
        {
        s[ 0 ] = '\0';
        return 0;
        }
    if( isspace( c ) )
        {                       /* indented line (or even a blank line?) */
        while( isspace( c ) && c != '\n' )
            {
            j = ( c == '\t' ) ? tabsize : 1;
            while( j-- )
                if( i<lim-1 )
                    s[ i++ ] = ' ';
            c = getchar();
            }
        if( c == '\n' || c == EOF )
            {
            i = 0;
            s[ i++ ] = '\n';    /* yeah, a blank line */
            }
        }
    else
        {                       /* read a word of non-blanks */
        while( isgraph( c ) )
            {
            if( i<lim-1 )
                s[i++] = c;
            else
                {
                fprintf( stderr, "warning: long input word (> %d chars)\n", lim );
                break;
                }
            c = getchar();
            }
        nonblanks += i;
        /* skip blanks after the word */
        while( isspace( c ) && c != '\n' )
            c = getchar();
        if( widespace )         /* check for end of sentence */
            if( i>2 && is_alnum[ s[ i-3 ] ]
             && !(i == 3 && is_cons[s[i-3]] && is_cons[s[i-2]]) )
                /* (makes sure that words like "g.", "e.g." and "Dr."
                   cannot create extra wide space) */
                switch( s[ i-1 ] )
                    {
                    case '.': case '!': case '?':
                        s[i++] = ' ';
                        break;
                    case ':':
                        if( widespace>1 )
                            s[i++] = ' ';
                        break;
                    }
        if( c == '\n' )         /* Just read the last word on this line */
            {
            /* did we read the first half of a split word? */
            if( unhyphen && i>1 && s[ i-1 ] == '-' && is_alnum[ s[ i-2 ] ] )
                s[i-1] = '\t';
            /* was this a "short line"? */
            if( nonblanks <= shortchars )
                s[i++] = '\n';
            nonblanks = 0;
            }
        }
    if( isgraph( c ) )          /* did we stop on a non-blank character? */
        ungetc(c, stdin);
    s[i] = '\0';
    return i;
    }


int scrollmode( UBYTE *stop_at, int imrg, int omrg )
/* copy line by line, only adjusting the left margin */
/* return value is the number of characters on the current output line */
    {
    int c, i, lchars;
    UBYTE s[WORDLEN];

    fflush(stdout);
    fprintf(stderr, "\e[2m");   /* highlight console output */
    fflush(stderr);

    s[ 0 ] = '\0';
    lchars = 0;
    c = getchar();
    while( c != EOF && strcmp( s, stop_at ) != 0 )
        {
        if (lchars == 0)
            {                   /* margin handling */
            for( i = 0; i<omrg; i++ )
                putchar(' ');
            i = imrg;
            while( i-- > 0 && isspace( c ) && c != '\n' )
                c = getchar();
            }
        i = 0;
        while( isgraph( c ) && i<WORDLEN-1 )
            {                   /* accumulate a word */
            s[ i++ ] = c;
            c = getchar();
            }
        s[ i ] = '\0';
        if( strcmp( s, stop_at) != 0 )
            {                   /* and (most likely) print it */
            printf( "%s", s );
            lchars += i;
            }
        else
            {                   /* no, it's the escape word! */
            if( keepescapes )
                {
                printf( "%s", s );
                lchars += i;
                }
            while( isspace(c) && c != '\n' )
                c = getchar();  /* swallow trailing blanks and exit */
            break;
            }
        while( isspace( c ) )
            {                   /* copy blanks between words */
            putchar( c );
            lchars++;
            if( c == '\n' )
                {
                c = getchar();
                lchars = 0;
                break;
                }
            else
                c = getchar();
            }
        }
    if( isgraph( c ) )          /* did we stop on a non-blank character? */
        ungetc( c, stdin );

    fflush( stdout );
    fprintf(stderr, "\e[0m");   /* normalize console output */
    fflush( stderr );

    return lchars;
    }


void init_tabs()
/* set up some tables to classify characters, assuming ISO-8859 charset */
    {
    int c;

    /* consonants: */
    for (c=0; c<256; c++)
        is_cons[c] = 0;
    for( c='a'; c<='z'; c++ )   /* standard consonants */
        switch(c)
            {
            case 'a': case 'e': case 'i': case 'o': case 'u': case 'y':
                break;
            default:
                is_cons[c] = 1;
            }
    for( c=224; c<256; c++ )    /* some from the ISO charset */
        switch(c)
            {
            case 231: case 240: case 241: case 254:
                is_cons[c] = 1; /* "ç", "ð", "ñ", "þ" */
                break;
            default:
                ;
            }
    for( c=32; c<256; c++ )     /* the corresponding uppercase letters */
        if( is_cons[c] )
            is_cons[c-32] = 1;
    is_cons[223] = 1;           /* "ß" */

    /* lowercase letters: */
    for( c=0; c<256; c++ )
        is_lower[c] = 0;
    for( c='a'; c<='z'; c++ )
        is_lower[c] = 1;
    for( c=224; c<256; c++ )
        is_lower[c] = 1;
    is_lower[247] = 0;          /* "÷" */
    is_lower[223] = 1;          /* "ß" */

    /* alphanumeric characters: */
    for( c=0; c<128; c++ )
        is_alnum[c] = isalnum(c);
    for( c=128; c<192; c++ )
        is_alnum[c] = 0;
    for( c=192; c<256; c++ )
        is_alnum[c] = 1;
    is_alnum[247] = 0;
    is_alnum[215] = 0;          /* "÷", "×" */
    }

