/**************************** x390tab.c *******************************/
/*****                                                            *****/
/***** This program was originally written by David Bond.  It has *****/
/***** been placed into the public domain and thus may be freely  *****/
/***** modified and redistributed with no restrictions whatsoever.*****/
/*****                                                            *****/
/*****   THERE IS NO WARRENTY FOR THIS PROGRAM, EITHER EXPRESS    *****/
/*****   OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED   *****/
/*****   WARRENTIES OF MERCHANTABILITY AND FITNESS FOR A          *****/
/*****   PARTICULAR PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY   *****/
/*****   AND PERFORMANCE OF THS PROGRAM IS WITH YOU.  SHOULD THE  *****/
/*****   PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL      *****/
/*****   NECESSARY SERVICING, REPAIR OR CORRECTION.               *****/
/*****                                                            *****/
/**********************************************************************/

/**********************************************************************/
/*                                                                    */
/*  This program should compile cleanly using most available ANSI C   */
/*  compilers.  It has been compiled using several different UNIX     */
/*  compilers as well as IBM's C Set++ and Visual Age C++ for OS/2    */
/*  and Microsoft's Visual C++.                                       */
/*                                                                    */
/*  This program uses the "access" function, which is part of UNIX    */
/*  and is implemented by many non-UNIX compiler libraries.  If       */
/*  your compiler does not support the "access" function, define      */
/*  the NOACCESS macro.                                               */
/*                                                                    */
/*  Two other macros may need to be defined:                          */
/*      UNIX  - if the program is being compiled for any UNIX flavor. */
/*      MSDOS - if the program is targeted for the MS-DOS or 16-bit   */
/*              OS/2 or Windows environments.  It should not be       */
/*              defined for 32-bit OS/2 or Windows.                   */
/*                                                                    */
/**********************************************************************/

#if defined(__IBMC__)
    #pragma strings(readonly)
#endif

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

#ifdef UNIX
    #define DIR_SEP '/'
    #define EOL_DEFAULT "\n"
    #include <unistd.h>
#else
    #define DIR_SEP '\\'
    #define EOL_DEFAULT "\r\n"
    #ifndef NOACCESS
        #include <io.h>
        #ifndef R_OK
            #define R_OK 04
            #define W_OK 02
        #endif
    #endif
#endif

/* Options */
static unsigned      uTestMode = 0;    /* 0 or 1 */
static unsigned      uTabIn    = 8;    /* 4 or 8 */
static unsigned      uTabOut   = 8;    /* 4 or 8 */
static unsigned      uCompress = 1;    /* 0 or 1 */
static unsigned      uLineMax  = 999;  /* 1 - 999 */
static char          szEol[3]  = EOL_DEFAULT; /* "\n" or "\r\n" */
static char          cRename   = '\0'; /* 'U', 'L', or '\0' */

/* Stuff derived from the options. */
static unsigned      uEolSize    = 0;
static unsigned      uTabInMask  = 0;
static unsigned      uTabOutMask = 0;
static unsigned      uMaxName    = 4;

/* Statistics */
static unsigned      uTotalFiles     = 0;
static unsigned long ulTotalLines    = 0;
static unsigned long ulTotalBytesIn  = 0;
static unsigned long ulTotalBytesOut = 0;

/* Current file */
static unsigned      uLineLength    = 0;
static unsigned long ulFileLines    = 0;
static unsigned long ulFileBytesIn  = 0;
static unsigned long ulFileBytesOut = 0;
static FILE        * pFileIn        = NULL;
static FILE        * pFileOut       = NULL;
static const char  * pszFileIn      = NULL;
static char          szFileOut[FILENAME_MAX];
static char          acLine[1001];

/************************************************************************/
/*                                                                      */
/*      iInitialize: handle the command line parameters                 */
/*                                                                      */
/*      passed:  command line argument count and parameters.            */
/*      returns: 0 (OK) or 1 (error)                                    */
/*                                                                      */
/************************************************************************/

static int iInitialize(
    int                argc,
    const char * const argv[])
{
    int          iError = 0;
    int          arg;
    const char * pszArg;
    static const char szUsage[] =
"Usage: x390tab [options] files\n\n"
"options: [-c  | -x ] compresses or expands the files using tab characters.\n"
"         [-d  | -u ] sets the end-of-line to DOS or UNIX standards.\n"
"         [-i4 | -i8] sets the input tab stops to every 4 or 8 characters.\n"
"         [-o4 | -o8] sets the output tab stops to every 4 or 8 characters.\n"
#ifndef MSDOS
"         [-rl | -ru] renames the files to lower or upper case names.\n"
#endif
"         [-lnnn]     sets the maximum line length to nnn (0-999).\n"
"         [-t]        sets test-only mode.\n"
#ifdef UNIX
"         default: -c -u -i8 -o8 -l999\n"
"files:   the files to be compressed or expanded.\n";
#else
"         default: -c -d -i8 -o8 -l999\n"
"files:   the files to be compressed or expanded (wild cards allowed).\n";
#endif

    for (arg = 1; arg < argc; ++arg) {
        pszArg = argv[arg];
        if (pszArg[0] != '-') {
            unsigned uLength;
#ifndef NOACCESS
            if (access(pszArg, R_OK | W_OK) != 0) {
                perror(pszArg);
                return 1;
            }
#endif
            uLength = strlen(pszArg);
            if (uMaxName < uLength)
                uMaxName = uLength;
            ++uTotalFiles;
            continue;
        }
        switch (toupper(pszArg[1])) {
          case 'C':
            if (pszArg[2] != '\0')
                iError = 1;
            else uCompress = 1;
            break;
          case 'D':
            if (pszArg[2] != '\0')
                iError = 1;
            else memcpy(szEol, "\r\n", 3);
            break;
          case 'H':
            fputs(szUsage, stderr);
            return 1;
          case 'I':
            if (((pszArg[2] != '4') &&
                 (pszArg[2] != '8')) ||
                (pszArg[3] != '\0'))
                iError = 1;
            else uTabIn = pszArg[2] - '0';
            break;
          case 'L':
            if ((pszArg[2] == '\0') ||
                (strlen(pszArg) > 5))
                iError = 1;
            else {
                uLineMax = 0;
                for (pszArg += 2; *pszArg != '\0'; ++pszArg)
                    if (!isdigit(*pszArg)) {
                        iError = 1;
                        break;
                    } else uLineMax = uLineMax * 10 + (*pszArg - '0');
                pszArg = argv[arg];
            }
            break;
          case 'O':
            if (((pszArg[2] != '4') &&
                 (pszArg[2] != '8')) ||
                (pszArg[3] != '\0'))
                iError = 1;
            else uTabOut = pszArg[2] - '0';
            break;
#ifndef MSDOS
          case 'R':
            if (((toupper(pszArg[2]) != 'L') &&
                 (toupper(pszArg[2]) != 'U')) ||
                (pszArg[3] != '\0'))
                iError = 1;
            else cRename = toupper(pszArg[2]);
            break;
#endif
          case 'T':
            if (pszArg[2] != '\0')
                iError = 1;
            else uTestMode = 1;
            break;
          case 'U':
            if (pszArg[2] != '\0')
                iError = 1;
            else memcpy(szEol, "\n", 2);
            break;
          case 'X':
            if (pszArg[2] != '\0')
                iError = 1;
            else uCompress = 0;
            break;
          default:
            iError = 1;
        }
        if (iError) {
            fprintf(stderr, "\"%s\" is not a valid command line option!\n",
                    pszArg);
            break;
        }
    }
    uEolSize    = strlen(szEol);
    uTabInMask  = 4096 - uTabIn;
    uTabOutMask = 4096 - uTabOut;

    if (!iError && (uTotalFiles == 0)) {
        fputs("No files were specified!\n", stderr);
        iError = 1;
    }
    if (iError)
        fputs(szUsage, stderr);
    else {
        unsigned u;
        fputs("File", stdout);
        if ((uTotalFiles > 1) && (uMaxName < 13))
            uMaxName = 13;
        for (u = 4; u < uMaxName; ++u)
            putc(' ', stdout);
        fputs("      Lines   Bytes in  Bytes out\n", stdout);
    }
    return iError;
} /* iInitialize */


/************************************************************************/
/*                                                                      */
/*      iReadLine:  read the next line from the input file.             */
/*                                                                      */
/*      passed:  nothing                                                */
/*      returns: 0 (OK), -1 (error), or 1 (eof)                         */
/*                                                                      */
/************************************************************************/

static int iReadLine(void)
{
    int c;

    if (feof(pFileIn))
        return 1;
    uLineLength = 0;
    memset(acLine, ' ', uLineMax);

    while ((c = getc(pFileIn)) != EOF) {
        ++ulFileBytesIn;
        switch (c) {
          case '\r':
            if ((c = getc(pFileIn)) == EOF) {
                if (feof(pFileIn))
                    return 0;
                break;
            }
            if (c != '\n')
                ungetc(c, pFileIn);
            else ++ulFileBytesIn;
            return 0;
          case '\n':
            return 0;
          case '\t':
            uLineLength = (uLineLength + uTabIn) & uTabInMask;
            if (uLineLength > uLineMax)
                uLineLength = uLineMax;
            break;
          case 26:
            if ((c = getc(pFileIn)) == EOF)
                break;
            ungetc(c, pFileIn);
            c = 26;
            /* fall through */
          default:
            if (uLineLength < uLineMax) {
                acLine[uLineLength] = (char)c;
                ++uLineLength;
            }
        }
        if (c == EOF)
            break;
    }

    if (feof(pFileIn))
        return (uLineLength == 0) ? 1 : 0;
    perror(pszFileIn);
    return -1;
} /* iReadLine */


/************************************************************************/
/*                                                                      */
/*      iWriteLine:  write the next line to the output file.            */
/*                                                                      */
/*      passed:  nothing                                                */
/*      returns: 0 (OK) or 1 (error)                                    */
/*                                                                      */
/************************************************************************/

static int iWriteLine(void)
{
    ++ulFileLines;
    while ((uLineLength != 0) && (acLine[uLineLength-1] == ' '))
        --uLineLength;

    if ((uCompress) && (uLineLength > uTabOut)) {
        unsigned uIn  = 0;
        unsigned uOut = 0;
        unsigned uTab;
        unsigned uPos;
        do {
            if (acLine[uIn] == ' ') {
                uTab = (uIn + uTabOut) & uTabOutMask;
                if (uTab > uLineLength)
                    uTab = uLineLength;
                if ((uTab - uIn) > 1) {
                    for (uPos = uIn+1; uPos < uTab; ++uPos)
                        if (acLine[uPos] != ' ')
                            break;
                    if (uPos == uTab) {
                        acLine[uOut++] = '\t';
                        uIn = uTab;
                        continue;
                    }
                }
            }
            acLine[uOut++] = acLine[uIn++];
        } while (uIn < uLineLength);
        uLineLength = uOut;
    }

    memcpy(acLine + uLineLength, szEol, uEolSize);
    uLineLength += uEolSize;
    if (!uTestMode &&
        (fwrite(acLine, 1, uLineLength, pFileOut) != uLineLength)) {
        perror(szFileOut);
        return 1;
    }
    ulFileBytesOut += uLineLength;
    return 0;
} /* iWriteLine */


/************************************************************************/
/*                                                                      */
/*      iProcessFile: process a file.                                   */
/*                                                                      */
/*      passed:  name of file to be processed                           */
/*      returns: 0 (OK) or 1 (error)                                    */
/*                                                                      */
/************************************************************************/

static int iProcessFile(
    const char * pszArg)
{
    int      iError   = 0;
    unsigned uPathLen = 0;

    /* Ignore command line parameters. */
    if (pszArg[0] == '-')
        return 0;
    pszFileIn      = pszArg;
    ulFileBytesOut = ulFileBytesIn = ulFileLines = 0;

    /* Open the input and output files. */
    if ((pFileIn = fopen(pszFileIn, "rb")) == NULL) {
        perror(pszFileIn);
        return 1;
    }
    if (uTestMode)
        pFileOut = NULL;
    else {
        const char * p = strrchr(pszFileIn, DIR_SEP);
        if (p != NULL) {
            uPathLen = 1 + (p - pszFileIn);
            memcpy(szFileOut, pszFileIn, uPathLen);
        }
        strcpy(szFileOut + uPathLen, "X390TAB.TMP");
        pFileOut = fopen(szFileOut, "wb");
        if (pFileOut == NULL) {
            perror(szFileOut);
            fclose(pFileIn);
            return 1;
        }
    }

    /* (de)compress the file. */
    do {
        if ((iError = iReadLine()) != 0) {
            iError = (iError < 0) ? 1 : 0;
            break;
        }
        iError = iWriteLine();
    } while (!iError);
    if (!iError) {
        unsigned u;
        fputs(pszFileIn, stdout);
        for (u = strlen(pszFileIn); u < uMaxName; ++u)
            putc(' ', stdout);
        fprintf(stdout, "%11lu%11lu%11lu\n",
                ulFileLines, ulFileBytesIn, ulFileBytesOut);
        ulTotalLines    += ulFileLines;
        ulTotalBytesIn  += ulFileBytesIn;
        ulTotalBytesOut += ulFileBytesOut;
    }

    /* Clean up and exit */
    fclose(pFileIn);
    if (!uTestMode) {
#ifndef MSDOS
        char szFileNew[FILENAME_MAX];
        strcpy(szFileNew, pszFileIn);
        if (cRename) {
            char * p;
            for (p = szFileNew + uPathLen; *p != '\0'; ++p)
                *p = (cRename == 'L') ? tolower(*p) : toupper(*p);
        }
#else
    #define szFileNew pszFileIn
#endif
        fclose(pFileOut);
        if (!iError &&
#ifdef UNIX
            (strcmp(szFileNew, pszFileIn) != 0) &&
#endif
            (remove(pszFileIn) != 0)) {
            fprintf(stderr, "Can't remove %s: %s\n",
                    pszFileIn, strerror(errno));
            iError = 1;
        }
        if (!iError && (rename(szFileOut, szFileNew) != 0)) {
            fprintf(stderr, "Can't rename %s to %s: %s\n",
                    szFileOut, szFileNew, strerror(errno));
            iError = 1;
        }
        if (iError)
            remove(szFileOut);
    }
    return iError;
} /* iProcessFile */


/************************************************************************/
/*                                                                      */
/*      main: Entry point for the tab/detab utility                     */
/*                                                                      */
/*      passed:  command line argument count and parameters.            */
/*      returns: highest return code.                                   */
/*                                                                      */
/************************************************************************/

int main(
    int                argc,
    const char * const argv[])
{
    if (iInitialize(argc, argv) != 0)
        return 1;

    {   int arg;
        for (arg = 1; arg < argc; ++arg)
            if (iProcessFile(argv[arg]) != 0)
                return 1;
    }

    if (uTotalFiles > 1) {
        unsigned u;
        fprintf(stdout, "\nTotal: %-6u", uTotalFiles);
        for (u = 13; u < uMaxName; ++u)
            putc(' ', stdout);
        fprintf(stdout, "%11lu%11lu%11lu\n",
                ulTotalLines, ulTotalBytesIn, ulTotalBytesOut);
    }

    return 0;
} /* main */

/**************************** x390tab.c *******************************/

