/* :ts=4 */
/*
 * MRPrint:     detabbing text file printer for the Amiga 
 * Author:      Mark Rinfret (Usenet: mrr@amanpt1.ZONE1.COM; Bix: markr)
 * 
 * I am offering this to the Amiga user community without restrictions. If you
 * make improvements, please re-release with source.      Enjoy!
 * 
 * 
 * This program will print text files containing embedded tabs and form feeds.
 * Though the default tab setting is 4, the user may override this to some
 * other value as necessary.  MRPrint will also optionally output a page
 * header containing the filename, current date and time, line number and
 * page number.  MRPrint supports variable margins and will enforce them.
 * Line numbers will be printed if requested.  Note that by default, MRPrint
 * prints to PRT:.  If you wish to redirect output, be sure to use the "-s"
 * option.
 * 
 * Usage:  pr [-l] [-n#] [-t#] [-h] [file1]...filen]
 *      options:    -h  do not print a page header 
 *                  -l  print with line numbers 
 *                  -L# set left margin to # 
 *                  -n# print # lines per page 
 *                  -R# set right margin to # 
 *                  -s  print to standard output 
 *                  -t# set tab to #spaces (default 4)
 * 
 * Handles ARP wildcarding.
 * 
 * 08/30/89 -MRR- V3.4: Fixed bug in line numbering.
 * 
 * 11/12/88 -MRR- Changed default margins to 1, 80, lines per page to 62.
 * 
 * 08/26/88 -MRR- When MRPrint detected a binary file, it printed a blank page
 * to "commemorate" the event.  Ugh!
 * 
 * 05/13/88 -MRR- Yeah, I know - version 3.0 didn't last very long. I observed
 * the output with the -s option and decided that the single character I/O I
 * was doing was very unacceptable.  This version buffers both input and
 * output.
 * 
 * 05/12/88 -MRR- THIS PROGRAM HAS BEEN ARPIFIED!  What the hell, I've been
 * wanting to dig into ARP for quite a while.  Now that I have V1.1 of ARP,
 * V3.6 of Manx and a day off, this was as good a program as any to do some
 * exploring.
 */

#define AMIGA
/* #define DEBUG */

#include <stdio.h>
#include <ctype.h>
#include <libraries/arpbase.h>
#include <arpfunctions.h>
#include <functions.h>

#define VERSION "pr version 3.4, 08/29/89 (requires ARP V1.1 or higher)"

#define INBUFSIZE       4096L   /* input buffer size */
#define MAXLINE         256
#define OUTBUFSIZE      2048L   /* output buffer size */
#define yes             1
#define no              0
#define SizeOf(x)       ((ULONG) sizeof(x))

/*
 * An extended AnchorPath structure to enable full pathnames to be generated
 * by FindFirst, FindNext.
 */

struct UserAnchor {
    struct AnchorPath ua_AP;
    BYTE            moreMem[255];
};

char           *FGets();        /* AmigaDOS/ARP compatible version. */
char           *NextFile();
void            PutNumber();
void            PutOneChar();
void            PutString();

unsigned        abort;          /* Set by CTRL-C, really unnecessary. */
struct UserAnchor *anchor;      /* Used by FindFirst, FindNext */
struct DateTime *dateAndTime;   /* Go ahead - take a wild guess. */
char            dateStr[20], timeStr[20];
unsigned        doLineNumbers = no;
unsigned        endOfInput;
BPTR            f;              /* The current input file (handle) */
char           *fileName;       /* The name of the input file. */
unsigned        forcePage;      /* Set by \f. */
unsigned        headers = yes;  /* Controls page header generation. */
UBYTE          *inBuf, *inBufPtr;       /* Input buffer, sliding pointer */
unsigned        inBufCount, inBufLength;
unsigned        leftMargin = 1;
unsigned        lineNumber;
unsigned        linesPerPage = 60;
UBYTE          *outBuf, *outBufPtr;     /* Output buffer, sliding pointer */
unsigned        outBufLength;   /* Length of output buffer. */
unsigned        pageNumber;
BPTR            printer;        /* Output device/file handle. */
static char    *prtname = "PRT:";
LONG            result;         /* Result of wildcard processing. */
unsigned        rightMargin = 80;
unsigned        srcLine;        /* Current source file line number. */
unsigned        tabSpace = 4;   /* How many spaces 1 tab equals. */
unsigned        tabStops[MAXLINE];      /* Computed tab stops. */
unsigned        useRequester = no;      /* Get filenames with requester? */
unsigned        useStdOut = no; /* Print to standard output? */
unsigned        xargc;          /* arg count after option processing */
char          **xargv;          /* arg vector after option processing */




/*
 * This is where all goodness begins.  Actually, I'm not too happy with the
 * size of the main program.  It ought to be broken up (or down :-).
 */
main(argc, argv)
    int             argc;
    char           *argv[];
{
    unsigned        i;
    char           *s;

    if (argc) {                 /* zero if started from workbench */
        ++argv;                 /* skip over program name arg */
        --argc;

        /* ..process switches.. */
        for (; *(s = *(argv)) == '-'; ++argv, --argc) {
            while (*++s)
                switch (*s) {
                case '?':
                    Usage();

                case 'l':
                    doLineNumbers = yes;
                    break;
                case 'L':
                    if ((leftMargin = Atol(s + 1)) <= 0) {
                        Abort("Bad left margin ", (long) leftMargin);
                    }
                    goto next_arg;      /* Oh my gawd!  A GOTO! */
                case 'n':
                    linesPerPage = Atol(s + 1);
                    goto next_arg;      /* Oh no!  A nuther one! */
                    break;
                case 'R':
                    if ((rightMargin = Atol(s + 1)) <= 0 ||
                        rightMargin > MAXLINE) {
                        Abort("Bad right margin ", (long) rightMargin);
                    }
                    goto next_arg;      /* It's a bloody epidemic! */
                case 's':
                    useStdOut = yes;
                    break;
                case 't':
                    if ((tabSpace = Atol(s + 1)) <= 0) {
                        Abort("Bad tab specification ", (long) tabSpace);
                    }
                    goto next_arg;      /* This is disgusting! */
                case 'h':
                    headers = no;
                    break;
                case 'v':
                    Printf("\n%s\n", VERSION);
                    break;
                default:
                    Usage();
                }
            /* Gag!  A label! There must be some goto's sneakin' around... */
    next_arg:;
        }
    }
    /* Check a few argument combinations. */

    if (leftMargin >= rightMargin) {
        Abort("Left margin >= right margin?  Ha ha!", 0L);
    }
    if (doLineNumbers)
        leftMargin = 5;         /* No margins with numbering but numbers use
                                 * 5 columns. */


    SetTabs();                  /* Initialize tab settings. */

    /* Allocate input and output buffers. */

    inBuf = ArpAlloc(INBUFSIZE);
    if (inBuf == NULL)
        Abort("No memory for input buffer!", INBUFSIZE);

    outBuf = ArpAlloc(OUTBUFSIZE);
    if (outBuf == NULL)
        Abort("No memory for output buffer!", OUTBUFSIZE);

    /* Get the date and time; we might need it. */

    dateAndTime = (struct DateTime *) ArpAlloc(SizeOf(*dateAndTime));
    if (dateAndTime == NULL) {
        Abort("No memory!", SizeOf(*dateAndTime));
    }
    DateStamp(dateAndTime);
    dateAndTime->dat_Format = FORMAT_USA;
    dateAndTime->dat_StrDate = dateStr;
    dateAndTime->dat_StrTime = timeStr;
    StamptoStr(dateAndTime);

    if (useStdOut)
        printer = (BPTR) Output();
    else if ((printer = ArpOpen(prtname, MODE_NEWFILE)) == NULL) {
        Abort("Failed to open printer ", IoErr());
    }
    /* Process files. */

    xargv = argv;
    if ((xargc = argc) == 0)    /* If no filename args, use requester. */
        useRequester = yes;
    else {
        if ((anchor = (struct UserAnchor *)
             ArpAlloc(SizeOf(*anchor))) == NULL) {
            Abort("No memory!", SizeOf(*anchor));
        }
        anchor->ua_AP.ap_Length = 255;  /* Want full path built. */
        anchor->ua_AP.ap_BreakBits |=
            (SIGBREAKF_CTRL_C | SIGBREAKF_CTRL_D);

        result = ERROR_NO_MORE_ENTRIES;
    }

    while (!abort && (fileName = NextFile())) {
        if ((f = (BPTR) Open(fileName, MODE_OLDFILE)) != NULL) {
            PrintFile();
            Close(f);
            f = NULL;
        } else
            Printf("\n*** MRPrint: Can't open %s for printing ***\n",
                   fileName);
    }
}

/*
 *  Abort the program. 
 *  Called with: 
 *      desc:           descriptive text 
 *      code:           error code (printed if non-zero) 
 *
 *  Returns: 
 *      to the system, where else?!
 */

Abort(desc, code)
    char           *desc;
    long            code;
{
    Printf("\n*** MRPrint aborting: %s", desc);
    if (code)
        Printf(" (%ld) ", code);
    Puts(" ***");
    if (f)
        Close(f);               /* File open? Close it. */
    ArpExit(20L, 0L);
}


/* Print one file. */

PrintFile()
{
    char            line[MAXLINE];

    forcePage = pageNumber = srcLine = 0;

    lineNumber = linesPerPage;

    inBufPtr = inBuf;
    inBufLength = 0;
    inBufCount = 0;
    outBufPtr = outBuf;
    outBufLength = 0;
    endOfInput = no;

    while (FGets(line, MAXLINE - 1, f) != NULL && !abort) {
        ++srcLine;              /* count input lines */

        /*
         * Note that top-of-form detection was a rather kludgy addition.  It
         * only works if the first character in the line is a ^L.
         */
        if (*line == '\f') {
            *line = ' ';        /* replace embedded ^L with blank */
            lineNumber = linesPerPage;  /* force new page */
        }
        if (lineNumber >= linesPerPage)
            Header();
        DeTab(line);            /* ..output detabbed line.. */

    }

    if (srcLine) {              /* We printed something? */
        PutOneChar('\f');       /* ..form-feed after last page.. */
        FlushBuffer();
    }
}

/*
 * An attempt has been made to print a line past the right margin. Crash the
 * user's system and melt his...naw, force a new line and output a new left
 * margin.  Also, if the page line count has been exceeded, start a new page.
 */

BreakLine()
{
    PutOneChar('\n');
    if (++lineNumber > linesPerPage)
        Header();
    DoLeftMargin();
}

/*
 * Output a dashed line according to an obscure algorithm derived through
 * intense empirical analysis while listening to the tune
 * 
 * "Camptown ladies sing this song, DoDash, DoDash..."
 */

DoDash()
{
    PutMany(' ', leftMargin);
    PutMany('-', rightMargin - leftMargin - 5);
    PutOneChar('\n');
}

/*
 * Output spaces for the left margin, or a source line number, whatever
 * tickles the user's fanny....fancy!
 */

DoLeftMargin()
{
    unsigned        i;

    if (doLineNumbers) {
        PutNumber(srcLine, 4);
        PutOneChar(' ');
    } else
        PutMany(' ', leftMargin);
}

/*
 * Print a header.
 */

Header()
{
    int             i;

    if (++pageNumber != 1) {
        PutOneChar('\f');       /* Eject if not first page. */
        PutOneChar('\n');
    }
    if (headers) {
        DoDash();

        /*
         * Note: there's room for improvement here.  A fancier algorithm
         * would attempt to distribute this information evenly over the
         * current page width.  A less lazy programmer would have written the
         * fancier algorithm.
         */
        PutString("     ");     /* Don't call DoLeftMargin! */
        PutString(fileName);
        PutMany(' ', 2);
        PutString(dateStr);
        PutMany(' ', 2);
        PutString(timeStr);
        PutString("  Page ");
        PutNumber(pageNumber, 0);
        PutString("  Line ");
        PutNumber(srcLine, 0);
        PutOneChar('\n');

        DoDash();

        PutString("\n");
    }
    lineNumber = 0;
}


/*
 * Replace embedded tab characters with the appropriate number of spaces,
 * outputting the results to the output device/file. 
 *
 * Called with: 
 *      line: string on which to do replacements 
 *
 * Returns: 
 *      eventually :-)
 */

DeTab(line)                     /* DeTab is not as good as DePepsi. */
    char           *line;
{
    int             eol = 0, i, col;

    DoLeftMargin();
    col = leftMargin;

    /*
     * Note: line[] has a terminating '\n' from fgets()...except if the input
     * line length exceeded MAXLINE.
     */
    for (i = 0; i < strlen(line); ++i)
        if (line[i] == '\t') {  /* ..tab.. */
            do {
                if (col == rightMargin) {
                    BreakLine();
                    break;
                }
                PutOneChar(' ');
                ++col;
            } while (!tabStops[col]);
        } else if (line[i] == 0x08) {   /* backspace? */
            if (col > 1) {
                PutOneChar(line[i]);
                --col;
            }
        } else {
            if (line[i] == '\n')
                ++eol;
            else if (col == rightMargin)
                BreakLine();
            PutOneChar(line[i]);
            ++col;
        }
    if (!eol)
        PutOneChar('\n');       /* no end of line? */
    ++lineNumber;
}

/* Initialize the tab settings for this file. */

SetTabs()
{
    int             i;

    for (i = 0; i < MAXLINE; ++i)
        tabStops[i] = (i % tabSpace == 1);
}


/* Display correct program Usage, then exit. */

Usage()
{
    register unsigned i;
    register char  *s;

    static char    *usageText[] = {
        "Usage:  pr [-l] [-n#] [-t#] [-h] [-v] [file1] file2] ...",
        "\toptions:",
        "\t\t-h      do not print page headers",
        "\t\t-l      print with line numbers",
        "\t\t-L#     set left margin to #",
        "\t\t-n#     print # lines per page",
        "\t\t-R#     set right margin to #",
        "\t\t-s      print to standard output instead of PRT:",
        "\t\t-t#     set tab to # spaces (default 4)",
        "\t\t-v      display program version number",
        "ARP wildcarding is supported.",
        (char *) NULL           /* last entry MUST be NULL */
    };

    for (i = 0; s = usageText[i]; ++i)
        Puts(s);

    ArpExit(20L, 0L);
}

/*
 * Get the next file name, either from the argument list or via a requester.
 */

char *
NextFile()
{
#define NUMBEROFNAMES   10L

    static struct FileRequester request;
    static char     dName[DSIZE * NUMBEROFNAMES + 1] = "";
    static char     fName[FCHARS + 1] = "";

    struct FileLock *lock;

    if (useRequester) {
        if (request.fr_File == NULL) {
            request.fr_File = fName;

            /*
             * To get the current directory path, get a lock on it, then use
             * PathName to convert it to a full path.
             */
            lock = Lock("", ACCESS_READ);
            PathName(lock, dName, NUMBEROFNAMES);
            UnLock(lock);
            request.fr_Dir = dName;
            request.fr_Hail = "Select file to print:";
        }
        return FileRequest(&request);
    }
    /*
     * Note: result is initialized to ERROR_NO_MORE_ENTRIES prior to calling
     * this routine for the first time.
     */

    while ((result == 0) || (result == ERROR_NO_MORE_ENTRIES)) {

        if (result == 0) {      /* Working a pattern? */
            if ((result = FindNext(anchor)) == 0L) {
                if (SkipDirEntry(anchor))
                    continue;
                break;
            }
        }
        if (result == ERROR_NO_MORE_ENTRIES) {
            if (xargc <= 0) {
                result = -1;
                break;
            }
            result = FindFirst(*xargv, anchor);
            ++xargv;            /* Advance arg list pointer. */
            --xargc;            /* One less arg to process. */
            if (result == 0) {
                if (SkipDirEntry(anchor))
                    continue;
                break;
            }
        }
        /* Only one error code is acceptable: */

        if (result && (result != ERROR_NO_MORE_ENTRIES)) {
            Printf("\n*** MRPrint I/O error %ld on pattern %s ***\n",
                   result, *xargv);
            result = 0;         /* Allow another pass. */
        }
    }

    /* Return filename or NULL, depending upon result. */
    return (result == 0 ? (char *) &anchor->ua_AP.ap_Buf : NULL);
}

/*
 * Read one line (including newline) from the input file. 
 * Called with: 
 *      line:       string to receive text 
 *      maxLength:  maximum length of string 
 *      f:          AmigaDOS file handle bee pointer (BPTR, ya' know).
 */

char *
FGets(line, maxLength, f)
    char           *line;
    int             maxLength;
    BPTR            f;
{
    char           *buf = line;
    int             c;
    int             lineLength = 0;

    if (abort = CheckAbort(NULL)) {
        PutString("\n^C\f");
        Abort("^C", 0L);
    }
    while (lineLength < maxLength) {
        if ((c = GetOneChar(f)) < 0)
            break;
        ++lineLength;
        if ((*buf++ = c) == '\n')
            break;              /* Stop on end of line. */
    }

    line[lineLength] = '\0';

    if (c < -1) {

        /*
         * Report the error to the printer and the console, but don't give up
         * on the rest of the files.  I think they call that being user
         * friendly.
         */
        c = -c;                 /* Invert the error code. */
        Printf("*** I/O error on input %d ***\n", c);

        if (!useStdOut) {
            PutString("*** Input I/O error");
            PutNumber(c, 0);
            PutString("***\n");
        }
        lineLength = 0;
    }
    return (lineLength == 0 ? NULL : line);
}

/* Flush the printer (output) buffer (phew!). */

FlushBuffer()
{
    long            actualLength;
    long            ioResult;

    if (outBufLength) {
        actualLength = Write(printer, outBuf, (long) outBufLength);
        if (actualLength != outBufLength) {
            ioResult = IoErr();
            Abort("Output error!", ioResult);
        }
    }
    outBufPtr = outBuf;
    outBufLength = 0;
}

/*
 * Get one character from the input stream.  If the input buffer is
 * exhausted, attempt to get some more input.  If this is the first input
 * buffer for this file, check the buffer for binary content. 
 *
 *  Called with: 
 *      f:      input file handle 
 *
 *  Returns: 
 *              character code (>= 0) or status (< 0, -1 => end of input)
 */

int
GetOneChar(f)
    BPTR            f;
{
    int             ioStatus;

    if (endOfInput)
        return -1;

    if (inBufLength <= 0) {
        inBufLength = Read(f, inBuf, INBUFSIZE);

        /*
         * If this is the first buffer, test it for binary content. If the
         * file is binary, skip it by setting the actualLength to zero
         * (simulate end of file).
         */
        if ((++inBufCount == 1) && inBufLength > 0) {
            if (SkipBinaryFile(anchor))
                inBufLength = 0;
        }
        if (inBufLength <= 0) {
            if (inBufLength == -1)
                ioStatus = -IoErr();
            else {
                ioStatus = -1;
                endOfInput = yes;
            }

            return ioStatus;
        }
        inBufPtr = inBuf;
    }
    --inBufLength;
    return *inBufPtr++;
}

/*
 * Put multiple copies of a character into the output buffer (repeat). 
 *
 *  Called with: 
 *      c:              character to be repeated 
 *      n:              number of copies 
 *
 *  Returns: 
 *                      tired but satisfied
 */

PutMany(c, n)
    int             c, n;

{
    for (; n > 0; --n)
        PutOneChar(c);
}

/*
 * Output a simple formatted unsigned number. 
 *
 * Called with: 
 *      number:     value to be formatted 
 *      length:     number of digits desired (0 => doesn't matter)
 */
void
PutNumber(number, length)
    unsigned        number, length;
{
    unsigned        digitCount = 0, i;
    char            digits[6];

    do {
        digits[digitCount++] = (number % 10) + '0';
        number /= 10;
    } while (number);

    while (length > digitCount) {
        PutOneChar(' ');
        --length;
    }

    do {
        PutOneChar(digits[--digitCount]);
    } while (digitCount);
}

/*
 * Output one character to the printer device/file. 
 *
 * Called with: 
 *      c:      character to be output 
 *
 * Returns: 
 *              nada
 */
void
PutOneChar(c)
    int             c;
{
    if (outBufLength >= OUTBUFSIZE)
        FlushBuffer();

    *outBufPtr++ = c;
    ++outBufLength;
}

/*
 * Output a string to the printer device/file. 
 *
 * Called with: 
 *      s:          string to output 
 *
 * Returns: 
 *                  when it's done, of course!
 */
void
PutString(s)
    char           *s;
{
    register int    c;
    register char  *s1;

    for (s1 = s; c = *s1; ++s1)
        PutOneChar(c);
}

/*
 * Test the contents of the first buffer for binary data.  If the buffer is
 * determined to have binary content, tell the user that we are skipping the
 * file.  This allows the user to give a single wildcard specification
 * without worrying about printing object, data and program files (assuming,
 * of course, that binary data is detected within the first INBUFSIZE bytes
 * of the file). 
 *
 * Called with: 
 *      anchor:         pointer to UserAnchor structure describing the file 

 * Returns: 
 *      yes:            file contains binary 
 *      no:             file is text (we think)
 */

int
SkipBinaryFile(anchor)
    struct UserAnchor *anchor;
{
    char           *strchr();

    /*
     * The following string describes binary characters that are considered
     * to be "OK".  These are, from left to right:
     * 
     * newline, form feed, tab, carriage return, backspace, ESCape
     * 
     */
    static char    *okSpecial = "\n\f\t\015\010\033";
    register UBYTE  c;
    register int    i;
    int             isBinary = no;

    for (i = 0; i < inBufLength; ++i)
        if (((c = inBuf[i]) < ' ') || c > 0x7F) {
            if (!strchr(okSpecial, c)) {
                isBinary = yes;
                break;
            }
        }
    if (isBinary) {
        Printf("\n*** MRPrint: skipping binary file %s ***\n", fileName);
    }
    return isBinary;
}

/*
 * Test the file described by the anchor parameter for "directoryness". If
 * it's a directory, print a message that we're skipping it. 
 * Called with: 
 *      anchor:         file entry info returned by FindFirst, FindNext 
 *
 * Returns:
 *      yes:            file is a directory 
 *      no:             file is a file (astonishing, eh?)
 */
int
SkipDirEntry(anchor)
    struct UserAnchor *anchor;
{
    if (anchor->ua_AP.ap_Info.fib_DirEntryType >= 0) {
        Printf("\n*** MRPrint: skipping directory %s ***\n",
               &anchor->ua_AP.ap_Buf);
        return yes;
    }
    return no;
}
