/*  File support routines to complement ARP.
 *  Author: Mark R. Rinfret
 *          Usenet:     mrr@amanpt1.Newport.RI.US
 *          BIX:        markr
 *          CIS:        72017, 136 (good luck!)
 *
 *          348 Indian Avenue
 *          Portsmouth, RI 02871
 *          401-846-7639 (home)
 *          401-849-9390 (work)       
 *
 *  This package was written primarily because of _one_ missing element
 *  in ARP: FGets. ARGH! ARPFFFT! It extends ARP by adding buffering to
 *  the basic file type (FileHandle) and defining a new type, named
 *  ARPFileHandle (hope this is OK with the ARP guys). Also, I've used the
 *  convention of embedding ARP (vs. Arp in MicroSmith's stuff) in all type
 *  and function names to (hopefully) avoid naming collisions.
 *
 *  This package (as far as I am concerned) is public domain. Use it any
 *  way you like. Some comments on the "MR" prefix: it isn't short for
 *  "Mister", it comprises the initials of my first and last names;
 *  I use it primarily to avoid name collisions with stuff by other
 *  authors. If any other authors whose initials are "MR" start doing
 *  this, we'll probably have to appeal to Electronic Arts to dole out
 *  "author codes" along the lines of those issued for IFF :-). I hereby
 *  stake a claim to 'MRR_'.
 *
 *  A word of warning:
 *
 *  DO NOT INTERMIX STANDARD AMIGADOS FILE SUPPORT CALLS WITH CALLS TO
 *  THIS PACKAGE ON FILES USING THIS FILE METHOD!
 *
 *  Obviously, the system doesn't know about the buffering added here
 *  and will cause unpredictable results (unless, of course, you
 *  take care to maintain the ARPFileHandle information).
 */

/*  Note: define DEBUG if you want the test program at the end. */

/*  Embedded documentation template (cut & paste): */

/*  FUNCTION
 *
 *  SYNOPSIS
 *
 *  DESCRIPTION
 *
 */

#include "MRARPFile.h"
#include <exec/memory.h>
#include <functions.h>

#ifndef min
#define min(a,b) ((a) <= (b) ? (a) : (b))
#define max(a,b) ((a) >= (b) ? (a) : (b))
#endif

/* StoreTracker is my attempt to fix an apparent bug in ARP 1.3. I have
 * found that the LastTracker kludge (via IoErr()) doesn't work reliably.
 * StoreTracker simply stuffs A1 into D0 and returns . It *MUST* be
 * called immediately after any ARP call which allocates a tracker to
 * assure that the value is correct.
 */

struct DefaultTracker *StoreTracker();  /* asm routine */

static LONG FillARPFileBuffer();

/*  FUNCTION
 *      FGetsARP - get string from a buffered ARP file.
 *
 *  SYNOPSIS
 *      char *FGetsARP(s, length, file)
 *                     UBYTE         *s;
 *                     LONG           length;
 *                     ARPFileHandle *file;
 *
 *  DESCRIPTION
 *      FGetsARP models the behavior of the "standard" fgets, except that
 *      it is used with a buffered ARP file. The data is read from <file>
 *      and transfered to string <s>. Up to length-1 characters will be
 *      read. Reading is also terminated upon receipt of a newline
 *      or detection of end-of-file. 
 *
 *      If the <file> was opened without a buffer, one of MaxInputBuf
 *      bytes will be allocated.
 *
 *      If end of file or an error is detected, the return value will be
 *      NULL. Otherwise, the return value will be <s>. <s> will be 
 *      terminated with a null byte.
 */
char *
FGetsARP(s, length, file)
    char            *s;
    LONG            length;
    ARPFileHandle   *file;
{
    LONG    bytesNeeded = length - 1;
    LONG    bytesRead = 0;
    char    c;
    char    *s1 = s;
    struct DefaultTracker *tracker;

    /* Set string initially empty to protect user from failure to check
     * return value.
     */
    *s1 = '\0';    
    if (file->mode != MODE_OLDFILE)
        file->lastError = ERROR_READ_PROTECTED;
         
    if (file->lastError) {
dangit:
        return NULL;
    }
    if (! file->buf ) {                 /* Ohmigosh! No buffer? */
        file->buf = ArpAllocMem(MaxInputBuf, MEMF_CLEAR|MEMF_PUBLIC);
        tracker = StoreTracker();
        if (! file->buf ) {
            file->lastError = ERROR_NO_FREE_STORE;
            goto dangit;
        }
        file->bufSize = MaxInputBuf;    /* bufLength, bufPos are zero. */
        file->bufTracker = tracker;
    }
    while (bytesNeeded) {
        if (file->bufPos >= file->bufLength) {
            if (FillARPFileBuffer(file) < 0) goto dangit;
            if (file->bufLength == 0) break;
        }
        c = file->buf[file->bufPos++];
        ++bytesRead;
        if (c == '\n') break;
        *s1++ = c;
        --bytesNeeded; 
    }
    *s1 = '\0';
    return (bytesRead ? s : NULL);
}

/*  FUNCTION
 *      FillARPFileBuffer - read data into ARPFile buffer.
 *
 *  SYNOPSIS
 *      static LONG FillARPFileBuffer(file)
 *                                    ARPFileHandle *file;
 *
 *  DESCRIPTION
 *      Attempt to fill the buffer associated with <file> by reading
 *      data from the associated external file. The return value will
 *      be one of the following:
 *          >0  => number of bytes read
 *           0  => end of file
 *          -1  => a Bad Thing happened (error code in file->lastError) 
 *
 *      Note: this is a local routine and is thus declared as "static".
 *      Please inform the author if this is a hardship.
 */
static LONG
FillARPFileBuffer(file)
    ARPFileHandle *file;
{
    /* Remember where we were. The user might want to try error
     * recovery. Of course, this probably isn't enough info, but
     * it's a start.
     */
    file->lastPosition = Seek(file->fh, 0L, OFFSET_CURRENT);
    file->bufPos = 0;
    file->bufLength = Read(file->fh, file->buf, file->bufSize);
    if (file->bufLength == -1) {    /* We got trubble! */
        file->lastError = IoErr();
    }
    else if (file->bufLength == 0) {
        file->endOfFile = TRUE;
    }
    return file->bufLength;
} 

/*  FUNCTION
 *      FlushARPFileBuffer - write file buffer contents to disk.
 *
 *  SYNOPSIS
 *      static LONG FlushARPFileBuffer(file)
 *                                     ARPFileHandle *file;
 *  DESCRIPTION
 *      FlushARPFileBuffer writes the contents of <file>'s buffer
 *      (if any) to disk and resets the buffer information. The
 *      return value may be any of:
 *
 *          >0 =>   number of bytes written
 *           0 =>   nothing in buffer
 *          <0 =>   negated error code
 *
 *      Note: it is assumed that this function will only be used locally
 *      and therefore need not be public. If you disagree, please contact
 *      the author.
 */

static LONG
FlushARPFileBuffer(file)
    ARPFileHandle *file;
{
    LONG    bytesWritten = 0;

    /* This operation is only allowed for output files. */

    if (file->mode != MODE_NEWFILE) {
        file->lastError = ERROR_WRITE_PROTECTED;
badstuff:
        return -file->lastError;
    }

    if (file->lastError) goto badstuff; /* Residual error? */

    if (file->bufLength) {
        file->lastPosition = Seek(file->fh, 0L, OFFSET_CURRENT);
        bytesWritten = Write(file->fh, file->buf, file->bufLength);
        if (bytesWritten != file->bufLength) {
            file->lastError = IoErr();
            goto badstuff;
        }
        else {
            file->bufLength = 0;
            file->bufPos = 0;
        }
    }
    return bytesWritten;
}

/*  FUNCTION
 *      FPutsARP - write a string to a buffered ARP file.
 *
 *  SYNOPSIS
 *      LONG FPutsARP(s, file)
 *                    char          *s;
 *                    ARPFileHandle *file;
 *
 *  DESCRIPTION
 *      FPutsARP writes the contents of string <s> to the specified <file>.
 *      If successful, it returns 0. On failure, it returns the negated
 *      system error code. 
 */
LONG
FPutsARP(s, file)
    char          *s;
    ARPFileHandle *file;
{
    LONG    bytesLeft, bytesUsed;
    char    *s1 = s;

    if (file->mode != MODE_NEWFILE) 
        file->lastError = ERROR_WRITE_PROTECTED;

    if (file->lastError) {
shucks:
        return -file->lastError;
    }

    bytesLeft = strlen(s);

    /* Attempt to be smart about this transfer. Copy the string to the
     * buffer in chunks. There is a possibility that the string is bigger
     * than the size of the buffer.
     */
    while (bytesLeft) {
        if (file->bufPos >= file->bufSize) {
            if (FlushARPFileBuffer(file) <= 0) goto shucks;
        }
        bytesUsed = min(file->bufSize - file->bufPos, bytesLeft);
        CopyMem(s1, &file->buf[file->bufPos], bytesUsed);
        s1 += bytesUsed;
        file->bufLength = (file->bufPos += bytesUsed);
        bytesLeft -= bytesUsed;
    }
    return 0;
}

/*  FUNCTION
 *      ReadARPFile - read from a buffered ARP file.
 *
 *  SYNOPSIS
 *      LONG ReadARPFile(file, buffer, length)
 *                       ARPFile *file;
 *                       UBYTE   *buffer;
 *                       LONG    length;
 *
 *  DESCRIPTION
 *      ReadARPFile attempts to read <length> bytes from <file>, transferring
 *      the data to <buffer>. 
 *
 *      The return value may be any of the following:
 *
 *          >0      number of bytes transferred
 *          0       end of file
 *         <0       negated error code
 *
 *      Note: if the lastError field of the <file> descriptor contains a
 *            non-zero value, its negated value will be returned and no
 *            attempt will be made to read the file. If you attempt error
 *            recovery, you must clear this field to zero.
 */
LONG
ReadARPFile(file, buffer, length)
    ARPFileHandle   *file;
    UBYTE           *buffer;
    LONG            length;
{
    LONG            bytesLeft;
    LONG            bytesRead = 0;
    LONG            needBytes = length;
    LONG            pos = 0;
    LONG            usedBytes = 0;

    /* Prevent read if this file opened for writing. */

    if (file->mode != MODE_OLDFILE)
        file->lastError = ERROR_READ_PROTECTED;

    if (file->lastError) {          /* Have residual error? */
boofar:
        return -file->lastError;
    }

    if ( ! file->buf ) {            /* No buffer? */
        bytesRead = Read(file->fh, buffer, length);
        if (bytesRead == -1) {
            file->lastError = IoErr();
            goto boofar;
        }
    }
    else while (needBytes) {
        if (file->bufLength - file->bufPos <= 0) {
            if (FillARPFileBuffer(file) == -1) goto boofar;
            if (file->bufLength == 0) break;
        }
        bytesLeft = file->bufLength - file->bufPos;
        usedBytes = min(bytesLeft, length);
        CopyMem(&file->buf[file->bufPos], &buffer[pos], usedBytes);
        file->bufPos += usedBytes;
        pos += usedBytes;
        bytesRead += usedBytes;
        needBytes -= usedBytes;
    }
    return bytesRead;   
}

/*  FUNCTION
 *      CloseARPFile - close buffered ARP file.
 *
 *  SYNOPSIS
 *      LONG CloseARPFile(file)
 *                        ARPFileHandle *file;
 *
 *  DESCRIPTION
 *      CloseARPFile closes the file described by <file> which MUST have
 *      been opened by OpenARPFile. It releases all tracked items
 *      associated with <file>, as well.
 *
 *      The return value is only meaningful for output files. If, upon
 *      flushing the buffer, a write error is detected, a system error
 *      code (ERROR_DISK_FULL, etc.) will be returned.
 */

LONG
CloseARPFile(file)
    ARPFileHandle *file;
{
    LONG        result = 0;

    if (file) {                     /* Just in case... */
        if (file->fileTracker) {
            /* Any left-over stuff in the buffer? If so, we must flush
             * it to disk. However, if an error was detected in the
             * previous operation, punt. 
             */
            if ( ( file->mode == MODE_NEWFILE) && 
                   ! file->lastError &&
                   file->bufLength) {
                if (Write(file->fh, file->buf, file->bufLength) !=
                    file->bufLength)
                    result = IoErr();
            }
            FreeTrackedItem(file->fileTracker);
        }

        if (file->bufTracker)
            FreeTrackedItem(file->bufTracker);

        FreeTrackedItem(file->myTracker);
    }
    return result;
}

/*  FUNCTION
 *      OpenARPFile - open a buffered ARP file
 *
 *  SYNOPSIS
 *      struct MRARPFile *OpenARPFile(name, accessMode, bytes)
 *                                    char *name;
 *                                    LONG accessMode, bytes;
 *
 *  DESCRIPTION
 *      OpenARPFile opens the file <name>, with the given <accessMode>
 *      (MODE_OLDFILE, MODE_NEWFILE only!) for buffered access. The
 *      size of the local buffer is specified by <bytes>.
 *
 *      A zero value for <bytes> is OK and is sometimes appropriate, as
 *      when performing file copy operations, etc. However, if a file
 *      opened with a zero length buffer is then passed to the
 *      FGetsARP function, "spontaneous buffer allocation" will occur.
 *      No biggy, really, just something you should know. The ARP constant,
 *      MaxInputBuf will be used for the buffer size, in this case.
 *
 *      If successful, a pointer to the file tracking structure is 
 *      returned. Otherwise, the return value will be NULL.
 *
 *      OpenARPFile uses full resource tracking for all information
 *      associated with the file.
 *
 */

ARPFileHandle *
OpenARPFile(name, accessMode, bytes)
    char *name;
    LONG accessMode, bytes;
{
    BPTR                  fh;
    ARPFileHandle         *theFile = NULL;
    struct DefaultTracker *lastTracker;

    /* This package does not support READ/WRITE access! */

    if ( (accessMode != MODE_OLDFILE) && (accessMode != MODE_NEWFILE))
        return NULL;

    /* 
     * Note: I'm using ArpAllocMem vs. ArpAlloc here because it appears
     * that ArpAlloc gives me a bad tracker pointer (?).
     */
    theFile = ArpAllocMem((LONG) sizeof(ARPFileHandle), 
                          MEMF_PUBLIC|MEMF_CLEAR);
    lastTracker = StoreTracker();
    if (theFile) {
        theFile->myTracker = lastTracker;
        theFile->mode = accessMode;
        fh = ArpOpen(name, accessMode);
        lastTracker = StoreTracker();
        if (!fh) {
fungu:
            CloseARPFile(theFile);  /* Don't worry - it's "smart". */
            theFile = NULL;
        }
        theFile->fileTracker = lastTracker;
        theFile->fh = fh;
        if ( bytes) {               /* Does user want a buffer? */
            theFile->buf = ArpAllocMem(bytes, MEMF_PUBLIC|MEMF_CLEAR);
            lastTracker = StoreTracker();
            if (!theFile->buf) goto fungu;
            theFile->bufSize = bytes;
            theFile->bufTracker = lastTracker;
        }
    }
    return theFile;  
}

/*  FUNCTION
 *      SeekARPFile - move to new logical position in file.
 *
 *  SYNOPSIS
 *      LONG SeekARPFile(file, position, mode)
 *                       ARPFileHandle *file;
 *                       LONG           position;
 *                       LONG           mode;
 *
 *  DESCRIPTION
 *      SeekARPFile attempts to position the <file> to a new logical
 *      position or report it's current position. The <position>
 *      parameter represets a signed offset. The <mode> parameter may
 *      be one of:
 *
 *          OFFSET_BEGINNING (-1) => from beginning of file
 *          OFFSET_CURRENT (0)    => from current position
 *          OFFSET_END (-1)       => from end of file
 *
 *      On output files, the current buffer contents, if any, are
 *      written to disk.
 *
 *      The return value will either be a positive displacement >=0) or
 *      a negated system error code.
 */
LONG
SeekARPFile(file, position, mode)
            ARPFileHandle *file;
            LONG           position;
            LONG           mode;
{
    LONG    newPosition;

    if (file->mode == MODE_NEWFILE && file->bufLength) {
        if (FlushARPFileBuffer(file) < 0) {
farboo:
            return -file->lastError;
        }
    }
    /* Remember our last position. */
    file->lastPosition = Seek(file, 0L, OFFSET_CURRENT);
    if ((newPosition = Seek(file->fh, position, mode)) == -1) {
        file->lastError = IoErr();
        goto farboo;
    }
    return newPosition;
}  

/*  FUNCTION
 *      WriteARPFile - write data to a buffered ARP file.
 *
 *  SYNOPSIS
 *      LONG WriteARPFile(file, buffer, length)
 *                        ARPFileHandle *file;
 *                        UBYTE         *buffer;
 *                        LONG          length;
 *
 *  DESCRIPTION
 *      WriteARPFile attempts to write <length> bytes from <buffer> to
 *      the buffered ARP file, <file>. The file MUST have been opened
 *      with OpenARPFile for MODE_NEWFILE access.
 *
 *      WriteARPFile will not write to a file if the lastError field is
 *      non-zero, or if the file was opened for input.
 *
 *      If successful, WriteARPFile will return the <length> parameter.
 *      Otherwise, it will return a negated system error code.
 */
LONG
WriteARPFile(file, buffer, length)
    ARPFileHandle   *file;
    UBYTE           *buffer;
    LONG            length;
{
    LONG    bufferBytes;
    LONG    bytesLeft = length;
    LONG    pos = 0;
    LONG    usedBytes = 0;

    if (file->mode != MODE_NEWFILE)
        file->lastError = ERROR_WRITE_PROTECTED;

    if (file->lastError) {           /* Catches mode and residual errors. */
sumbidge:
        return -file->lastError;
    }

    if ( ! file->buf ) {             /* No buffer? */
        if (Write(file->fh, buffer, length) != length) {
            file->lastError = IoErr();
            goto sumbidge;
        }
    }
    else while (bytesLeft) {
        /* Need to flush the file's buffer? */
        if (file->bufPos >= file->bufSize) {
            if (FlushARPFileBuffer(file) < 0) {
                goto sumbidge;
            }
        }
        bufferBytes = file->bufSize - file->bufPos;
        usedBytes = min(bufferBytes, bytesLeft);
        CopyMem(&buffer[pos], &file->buf[file->bufPos], usedBytes);
        file->bufLength = (file->bufPos += usedBytes);
        pos += usedBytes;
        bytesLeft -= usedBytes;
    }
    return length;
}

#ifdef DEBUG
/*  Short test program. */

struct ArpBase          *ArpBase;
struct IntuitionBase    *IntuitionBase;
struct GfxBase          *GfxBase;

ARPFileHandle *
GetFile(prompt, mode, bufferSize)
    char    *prompt;
    LONG    mode, bufferSize;
{
    BOOL            keepTrying;
    ARPFileHandle   *theFile = NULL;
    char            fileName[MaxInputBuf+1];

    for (keepTrying = TRUE; keepTrying; ) {
        Puts("\nA null response aborts this test.");
        Puts(prompt);
        ReadLine(fileName);
        if ( ! *fileName ) break;   /* Null response? */
        theFile = OpenARPFile(fileName, mode, bufferSize);
        if (theFile == NULL) {
            Puts("That file failed to open. Try again.\n");
        }
        else
            keepTrying = FALSE;
    }
    return theFile;
}

main()
{
#define BUFFER_SIZE     16384L

static char line[81];

    UBYTE           *buffer;
    LONG            count;
    ARPFileHandle   *file1, *file2;
    int             keepTesting;
    LONG            totalBytes;

    ArpBase = (struct ArpBase *) OpenLibrary(ArpName, ArpVersion);
    if (ArpBase == NULL) {
        exit(1);
    }
    GfxBase = (struct GfxBase *) ArpBase->GfxBase;
    IntuitionBase = (struct IntuitionBase *) ArpBase->IntuiBase;

    Puts("MRARPFile package test.\n\n");

FGetsARPTest:
    Puts("FGetsARP test - echo file to console.");
    file1 = GetFile("Enter the name of a text file:", MODE_OLDFILE, 4192L);
    if ( file1) {
        while (FGetsARP(line, (LONG) sizeof(line), file1))
            Puts(line);
        CloseARPFile(file1);
    }

CopyFileTest:
    buffer = ArpAllocMem(BUFFER_SIZE, MEMF_PUBLIC);
    if ( ! buffer ) {
        Printf("I couldn't allocate a %ld byte buffer!\n", BUFFER_SIZE);
        goto die;
    }
    for (keepTesting = TRUE; keepTesting; ) {
        Puts("ReadARPFile/WriteARPFile test.");
        Puts("For this test, I will copy a file.");
        file1 = GetFile("Enter input file name:", MODE_OLDFILE, 512L);
        if (!file1) break;
        file2 = GetFile("Enter output file name:", MODE_NEWFILE, 8192L);
        if (!file2) {
            CloseARPFile(file1);
            break;
        }
        keepTesting = FALSE;        /* This is it... */
        totalBytes = 0;
        while (count = ReadARPFile(file1, buffer, BUFFER_SIZE)) {
            totalBytes += count;
            if (WriteARPFile(file2, buffer, count) != count) {
                Printf("Error on output file: %ld\n", file2->lastError);
                break;
            }
            if (count < BUFFER_SIZE) break;
        }
        if (file1->lastError)
            Printf("Error on input file: %ld\n", file1->lastError);
        CloseARPFile(file1);
        CloseARPFile(file2);
        Printf("Bytes transferred: %ld\n", totalBytes);
        /* Note: leave the buffer for ARP to free up. You should check
         * available memory before/after running this test to make sure
         * that ARP does proper housecleaning.
         */
    }
die:
    CloseLibrary(ArpBase);
    exit(0);
}

#endif