/* $Id: junk_filter.cc,v 1.40 1998/06/14 06:25:36 dps Exp $ */

/* This code filters out almost all the "junk" in word documents and
* is useful for extracting the text from word documents. I think this
* program produces better signal to noise that catdoc. */

#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#ifdef HAVE_STRING_H
#include <string.h>
#endif /* HAVE_STRING_H */
#ifdef HAVE_STRINGS_H
#include <strings.h>
#endif /* HAVE_STRINGS_H */
#ifdef HAVE_CTYPE_H
#include <ctype.h>
#endif /* HAVE_CTYPE_H */
#include <stream.h>
#include <iostream.h>
#include <fstream.h>
#include "strip.h"
#include "tune.h"

/* Note: This is a two stage filter that outputs 0 or 1 characters
  per input character and thus can use the same buffer safely. */

/* Read more */
int word_junk_filter::__fill(const char *b, int n, int sp, int offset)
{
   int end, nr;

   if (n>0)
       memmove(buf+offset, b, n);
   offset+=n+sp;
   if (offset>JUNK_FILT_BUFSIZE)
   {
       cerr<<"junk_filter::__fill: start offset "<<offset<<" >buffer size"
           "(fatal)\n";
       exit(1);
   }
   if (offset<JUNK_FILT_BUFSIZE)
   {
       is->read(buf+offset, JUNK_FILT_BUFSIZE-offset);
       nr=is->gcount();
   }
   else
       nr=0;
   pos+=nr;
   end=nr+n;
   return end;
}


/* Junk filtering. Buffer size must be >=RESUME_CHARS */
int word_junk_filter::filter_junk(const char *b, int ns)
{
   int i, d, n, top_run;

   if ((n=__fill(b, ns, junk_end_usage, 1))==0)
       return 0;
   if (junk_end_usage>0)
       memcpy(buf+ns+1, junk_end_buf, junk_end_usage);
   if (init==INIT_SKIPPED)
   {
       init=INIT_DONE;         // Finished init
       ns=0;                   // Process all the data
       text_size=0;            // Text size is zero
   }

#ifdef DEBUG_FILTER_FILL
   printf("\n---refill ns=%d junk_end_usage=%d\n", ns, junk_end_usage);
   printf("---end=%d mode=%d\n", n, mode);
#endif

   /* Correctness of this loop depends on d<=i at the top of the
      loop. The claims * by inductioon about the fake variable __adj__ suffice
      to prove this. All the relevant commments are prefixed by CS */

   top_run=0;
   /*** CS: __adj__=1 ****/
   for (i=ns+1, d=ns; i<=n; i++)       // Ignore already processed data pushed back
   {
       /*** CS: i==d+junk_end_usage+__adj__ and __adj__>=0 here by induction ***/
       /*** CS: if mode==SKIP_JUNK or SKIP_JUNK_WASPRN then __adj__>=1       ***/
#ifdef DEBUG_FILTER
       printf("mode %d, char[%i] (t=%d) %c, asc=%d\n", mode, i, n,
              buf[i], junk_end_usage);
#endif
       switch(mode)
       {
       case NORMAL:
           /* Normal mode. OK becasue change in d at most 1 and junk_end_usage=0 */
           if (buf[i]==0)
           {
               junk_end_usage=0;
               junk_size=0;
               mode=SKIP_JUNK;
               break;
           }
           if (buf[i] & 0x80)
           {
               /*** CS: by induction junk_end_usage==top_run ***/
               junk_end_buf[junk_end_usage++]=buf[i];  // Save character
               top_run++;                              // Increment top run counter
               /*** CS: __adj__++ ***/
               if (top_run==tune.max_top_run)
               {
                   junk_size=top_run;                  // Top bit characters are junk
                   junk_end_usage=0;                   // Throw away characters
                   mode=SKIP_JUNK;                     // End junk skipping mode
                   /*** CS: __adj__ += top_run ***/
                   break;
               }
               break;                                  // Exit switch
           }
           /* Top bit not set */
           if (top_run)
           {
               memcpy(buf+d, junk_end_buf, junk_end_usage); // OK by ind hyp.
               d+=junk_end_usage;
               junk_end_usage=0;                       // Ind. hyp now true
               top_run=0;                              // Reset top_run counter.
           }
           /* Add character */
           buf[d++]=buf[i];
           text_size++;
           if (unicode!=__UNI_NO)
               mode=UNICODE_Z;
           break;

       case UNICODE_Z:
           /* unicode zero mode. OK because change in d is 0 and junk_end_usage=0 */
           if (buf[i]==0)
           {
               mode=NORMAL; // 0 => Back to printable character mode
               /*** CS: __adj__++ ***/
           }
           else
           {
#ifdef UNICODE_MARKERS
               printf("\n*** Mode %d: %d Got %d expected 0\n", mode,
                      UNICODE_Z, buf[i]);
#endif
               junk_end_usage=0;
               junk_size=0;
               mode=SKIP_JUNK;
               /*** CS: __adj__++ ***/
           }
           break;

       case SKIP_JUNK_WASPRN:
           /* Skip junk was printable, added for unicode documents */
           /* OK because junk_end_usage chages by at most 1 and d stays fixed */
           if ((++junk_size)==tune.max_junk && text_size>tune.min_text)
           {
#ifdef DEBUG_SINK_START
               cerr<<"Enter sink mode\n";
#endif /* DEBUG_SINK_START */
               /*** CS: __adj__++ ***/
               mode=SINK_JUNK; // Sink junk
               break;
           }
           if  (tune.options.unicode_aggresive)
           {
               /* Agresive unicode mode: insist on the MSBs appearing */
               if (unicode!=__UNI_NO)
               {
                   if (buf[i]!=0)
                   {
                       /*** CS: __adj__+=junk_end_usage ***/
                       junk_end_usage=0;
                   }
                   mode=SKIP_JUNK;
                   /*** CS: __adj__++ ***/
                   break;
               }
           }
           else
           {
               /* Non-aggersive unicode mode: simply skip the MSB */
               if (buf[i]==0 && unicode!=__UNI_NO)
               {
                   /*** CS: __adj__++ ***/
                   break;      // Accept zeros
               }
           }
           /* Fall thru */

       case SKIP_JUNK:
           /* Junk skipping mode */
           if ((++junk_size)==tune.max_junk && text_size>tune.min_text)
           {
#ifdef DEBUG_SINK_START
               cerr<<"Enter sink mode\n";
#endif /* DEBUG_SINK_START */
               mode=SINK_JUNK; // Sink junk
               /*** CS: __adj__++ ***/
               break;
           }
           if (isprint(buf[i]) || buf[i]=='\r' || buf[i]=='\n'
               || buf[i]=='\t')
           {
#ifdef DEBUG_RESTART
               printf("---Accepted *%c*, usage=%d\n", buf[i], junk_end_usage);
#endif
               junk_end_buf[junk_end_usage++]=buf[i]; // Save character
               if (unicode!=__UNI_NO)
                   mode=SKIP_JUNK_WASPRN; // For unicode documents
           }
           else
           {
               /* Reset junk buffer usage counter */
               /*** CS: __adj__+=junk_end_usage+1 ***/
#ifdef DEBUG_RESTART
               if (junk_end_usage!=0)
               printf("---Rejected %d, usage=%d\n", buf[i], junk_end_usage);
#endif
               junk_end_usage=0;
               break;
           }
           if (junk_end_usage<tune.resume_chars)
               break;
           /* Turn nomral mode back on */
#ifdef DEBUG_RESTART
           printf("---back normal mode count=%d\n", junk_size);
#endif
           /* The __adj__ analysis is *vital* here; if __Adj__=0 then d>i occurs */
           /* however forutnately this is impossible by the claims that __adj__  */
           /* and indcution proves.                                              */

           /**** CS: __adj__>=1 here by indcution. Thus i>=d+junk_end_usage+1 ****/
           mode=(unicode==__UNI_NO) ? NORMAL : UNICODE_Z;
           buf[d]=0x7f;                                // Mark suspect position
           memcpy(buf+d+1, junk_end_buf, junk_end_usage); // Nothing clobbered by [1]
           d+=junk_end_usage+1;                        // Advance d
           junk_end_usage=0;                           // Clear junk end buffer
           /*** CS: __adj__-- ***/
           break;

       case SINK_JUNK:
           /*** CS: __adj__++ ***/
           break;

       default:
           cerr<<"word_junk_filter::filter_junk: Invalid mode "<<mode
               <<" (fatal)\n";
           exit(1);
       }
   }
   return d;
}

int word_junk_filter::underflow(void)
{
   int s,d,last;
   int end;

   do                          // While no characters
   {
       /* Fill the buffer */
       if (init==INIT_NONE)
       {
           if (is!=NULL)
           {
               init=INIT_SKIPPED;
               ns=this->skip_to_start();
               end=this->filter_junk(buf, ns);
           }
           else
               return EOF;     // If no input stream return EOF
       }
       else
       {
           end=this->filter_junk(&save, ns);
           ns=0;
       }

       /* Check for EOF */
       if (end==0 && is->eof())
           return EOF;

       ns=0;
       /* Collapse multi-character sequeneces */
       for (s=0, d=0, last=-1; s<end; s++)
       {
           switch(buf[s])
           {
           case '\n':
               if (last=='\r')
                   continue;   // \n \r to \r
               break;


           case '\r':
               if (last=='\n')
                   d--;        // \r \n to \r
               break;


           case 0x07:
               if (last=='\r')
                   d--;        // \n <TAB SEP> to <TAB SEP>
               break;

           default:
               break;
           }
           last=buf[d]=buf[s];
           d++;
       }

       if (end!=1 && (buf[d-1]=='\r' || buf[d-1]=='\n'))
       {
           d--;
           ns=1;
           save=buf[d];
       }
   } while (d==0);
   setg(buf, buf, buf+d);
   return(buf[0] & 0xff);
}


/* Skip to start */
int word_junk_filter::skip_to_start(void)
{
   int zc,ffc,c,n,i,rej,limit=-1;
   char back[2*ST_ASC_VCHARS_UNICODE+ST_REJ_LIMIT+4];
   int bp;
   long pos=0;

   enum { FIND_BEGIN, SKIP_ONE, FIND_ASC, FIND_ASC_WASASC, DONE } mode;
   zc=0; ffc=0; i=0; n=0; bp=0; rej=-1;
   mode=FIND_BEGIN;
   while (mode!=DONE)
   {
       /* If need a refill */
       if (i==n)
       {
           /* Fill buffer */
           n=this->__fill(back,bp,0,1);
           if (n==0)
               return 0;
           pos+=n-bp;
#ifdef DEBUG_SKIP_FILL
           printf("---skip--- refill bp=%x, pos=%x\n", bp, pos);
#endif
           /* Ignore first saved character */
           if (mode==FIND_ASC)
               mode=SKIP_ONE;
           i=0;
           bp=0;
       }

#ifdef DEBUG_SKIP
       printf("char %c mode %d, c=%d, pos=%x, i=%d l=%d, rej=%d\n", buf[i+1],
                  mode, c, pos++, i, limit, rej);
#endif
       switch(mode)
       {
       case FIND_BEGIN:
           switch(buf[i+1])
           {
           case 0:
               zc++;
               ffc=0;
               break;

           case ((char) 0xff):
               ffc++;
               zc=0;
               break;

           default:
               if (zc>=tune.st_min_zeros ||
                   (ffc>=tune.st_min_ff && tune.options.ff_intro))
               {
                   unicode=__UNI_PROBE;
                   back[bp++]=buf[i+1];
                   c=0;
                   rej=0;
                   mode=FIND_ASC_WASASC; // Ignore this character deliberately
               }
               else
               {
                   zc=0;
                   ffc=0;
               }
               break;
           }
           break;

       case SKIP_ONE:
           back[bp++]=buf[i+1];
           c=0;
           rej=0;
           mode=FIND_ASC_WASASC;
           break;

       case FIND_ASC_WASASC:   // unicode support
           if (buf[i+1]==0 && unicode!=__UNI_NO)
           {
               if (unicode==__UNI_PROBE)
               {
                   limit=tune.unicode_st; // Start characters needed
                   /* FIXME: Anyone got a better method of doing this? */
                   if ((pos+i) % 2==0)
                   {
                       unicode=__UNI_YES_BE; // 0 is first (even) bu#yte
                   }
                   else
                   {
                       unicode=__UNI_YES_LE; // 0 is second (odd) byte
                   }
                   back[bp++]=buf[i+1];
                   mode=FIND_ASC;            // Inist next character not zero
#ifdef DEBUG_SKIP_UNI
                   cerr<<"Characters are unicode "
                       <<((unicode==__UNI_YES_LE) ? "litle" : "big")
                       <<" endian\n";
#endif
               }
               else
                   back[bp++]=buf[i+1];
               c++;
               break;
           }
           else if (unicode==__UNI_PROBE)
           {
#ifdef DEBUG_SKIP_UNI
               if (unicode==__UNI_PROBE)
                   cerr<<"Characters are not unicode\n";
#endif
               limit=tune.non_unicode_st; // Start characters needed
               unicode=__UNI_NO;          // Non unicode
               mode=FIND_ASC;             // Finding printable mode
           }
           /* fall thru */

       case FIND_ASC:
           if (isprint(buf[i+1]) || buf[i+1]=='\r' || buf[i+1]=='\n'
               || buf[i+1]=='\t')            // If is printable
           {
               back[bp++]=buf[i+1];          // Store character
               c++;                          // Increase count
               if (unicode!=__UNI_NO)
                   mode=FIND_ASC_WASASC;     // Unicode hack...
           }
           else
           {
               back[bp++]=buf[i+1];          // Store caharcetr
               if (buf[i+1]!=0)
                   rej++;                    // Adjust reject count
               if (buf[i+1]==0 || rej>tune.st_rej_limit) // 0 or reject limit
               {
                   bp=0;                     // Reset buffer pointer
                   unicode=__UNI_PROBE;      // Set unicode status to unknonw
                   ffc=(buf[i+1]== (char) 0xff) ? 1 : 0; // reset ff count
                   zc=(buf[i+1]==0) ? 1 : 0; // reset zeros count
                   mode=FIND_BEGIN ;         // Find leading characters mode
               }
           }
           if (limit==-1)                    // Bug catcher
           {
               cerr<<"Bug: limit still -1 at point #1\n";
               exit(2);
           }
           if (c==limit)                     // IF found enough
           {
#ifdef DEBUG_SKIP
               printf("***** Enter exit phase ****\n");
#endif
               mode=DONE;                    // Finished skipping
           }
           break;

       case DONE:
           cerr<<"skip_to_start: loop called in done mode\n";
           break;

       default:
           cerr<<"skip_to_start: Mode invalid\n";
           break;
       }
       i++;
   }
#ifdef DEBUG_SKIP_MOVE
   printf("%c%c%c\n", buf[i-bp+1], buf[i-bp+2], buf[i-bp+3]);
#endif
   memmove(buf, buf+i-bp+1, n-i+bp);         // Move memory
   return(n-i+bp);                           // Return count
}


/* Option handling stuff for tuning this module. Note that only long options
* are supported. If you lack long options these features are not avialable. */
/* set default tuning */
void word_junk_filter::set_dfl_tuning(void)
{
#ifdef AGGRESIVE_UNICODE
   tune.options.unicode_aggresive=AGGRESIVE_UNICODE; // Aggresive unicode mode
#else
   tune.options.unicode_aggresive=1;                 // Aggresive unicode mode
#endif /* AGGRESIVE_UNICODE */
   tune.resume_chars=RESUME_CHARS;                   // Resume characters
   tune.st_min_zeros=ST_MIN_ZEROS;                   // Minimum zeros
   tune.st_rej_limit=ST_REJ_LIMIT;                   // Reject chars tolerance
   tune.st_min_ff=ST_MIN_FF;                         // Minimum ff's (alternative to 0s)
   tune.options.ff_intro=ALLOW_FF_LEADIN;            // Enable leading ff's
   tune.non_unicode_st=ST_ASC_VCHARS;                // ok characters leadin
   tune.unicode_st=ST_ASC_VCHARS_UNICODE;            // ok unicode characters
   tune.min_text=SINK_MIN_TEXT;                      // text required before sink mode
   tune.max_junk=SINK_JUNK_CNT;                      // junk required for sink mode
   tune.max_top_run=MAX_TOP_RUN;                     // Max run length of top bits set
}

int word_junk_filter::do_reset(void)
{
       pos=0;
       init=INIT_NONE;
       mode=NORMAL;
       junk_end_usage=0;
       ns=0;
       return 1;               // Success
}