/*
 * punches - convert beteen IBM1130 simulator binary card image format and ascii text lists of punch numbers
 *
 * Usage:
 *      punches -b [infile [outfile]]
 *          Converts from ascii to binary. Reads stdin/writes stdout if infile/outfile not specified
 *
 *      punches -a [infile [outfile]]
 *          Converts from binary to ascii.
 *
 * The ASCII format consists of an arbitrary number of card images. Each card image consists of
 * a line with the word "start", followed by 80 lines each containing the punch data for one card
 * column, followe by a line with the word "end".
 *
 * A column specification line consists of the word "blank", for a column with no punches,
 * or an arbitrary number of integer row names separated by hyphens. The row names are 12, 11, 0, 1, 2, ..., 9.
 *
 * The character #, * or ; terminates an input line and the remainder of the line is ignored as a comment.
 * Blank lines are ignored and may occur at any place in the input file.
 *
 * A typical card specification might look like this:
 
   start
   * This is a comment line
   12-1-2
   blank
   5         # this is a comment after the data for column 3
   4
   2
   blank
   blank
   11-5
   2-6-3
.                     \
.                      | not all lines shown. Exactly 80 data lines are required
.                     /
blank
12-0-2-6-7-8
end

 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "util_io.h"
#ifdef WIN32                    // for Windows binary file mode setting
#  include <io.h>
#  include <fcntl.h>
#endif

#define TRUE  1
#define FALSE 0
typedef int BOOL;

#define BETWEEN(v,a,b) (((v) >= (a)) && ((v) <= (b)))

BOOL failed = FALSE;
int ncards = 0;

void tobinary (char *fnin, char *fnout);
void toascii (char *fnin, char *fnout);
void bail (char *msg);

int main (int argc, char **argv)
{
    enum {MODE_UNKNOWN, MODE_TOBINARY, MODE_TOASCII} mode = MODE_UNKNOWN;
    int i;
    char *arg, *fnin = NULL, *fnout = NULL;
    static char usestr[] = "Usage: punches -b|-a [infile [outfile]]";

    for (i = 1; i < argc; i++) {
        arg = argv[i];
        if (*arg == '-') {
            arg++;
            while (*arg) {
                switch (*arg++) {
                    case 'b':
                        mode = MODE_TOBINARY;
                        break;

                    case 'a':
                        mode = MODE_TOASCII;
                        break;

                    default:
                        bail(usestr);
                }
            }
        }
        else if (fnin == NULL)
            fnin = arg;
        else if (fnout == NULL)
            fnout = arg;
        else
            bail(usestr);
    }

    util_io_init();                             // check CPU for big/little endianness

    if (mode == MODE_TOBINARY)
        tobinary(fnin, fnout);
    else if (mode == MODE_TOASCII)
        toascii(fnin, fnout);
    else
        bail(usestr);

    if (failed) {
        if (fnin != NULL) {                     // if there was an error, delete output file if possible
            unlink(fnout);
            fprintf(stderr, "Output file \"%s\" deleted\n", fnout);
            exit(1);
        }
        else
            bail("Output file is incorrect");
    }
    else                                        // if no error, tell how many cards we converted
        fprintf(stderr, "* %d card%s converted\n", ncards, (ncards == 1) ? "" : "s");

    return 0;
}

// alltrim - remove string's leading and trailing whitespace

char *alltrim (char *str)
{
    char *c, *e;

    for (c = str; *c && *c <= ' '; c++)         // skip over leading whitespace
        ;

    if (c > str)                                // if there was some, copy string down over it
        strcpy(str, c);

    for (e = str-1, c = str; *c; c++)           // find last non-white character
        if (*c > ' ')
            e = c;

    e[1] = '\0';                                // terminate string immediately after last nonwhite character
    return str;                                 // return pointer to string
}

void tobinary (char *fnin, char *fnout)
{
    FILE *fin, *fout;
    BOOL gotnum;
    int col, v, lineno = 0;
    char str[256], *c;
    unsigned short buf[80], punches;
    static unsigned short punchval[13] = {
        0x2000, 0x1000, 0x0800, 0x0400, 0x0200,         // 0, 1, 2, 3, 4
        0x0100, 0x0080, 0x0040, 0x0020, 0x0010,         // 5, 6, 7, 8, 9
        0x0000,                                         // there is no 10 punch
        0x4000, 0x8000};                                // 11 and 12.

    if (fnin == NULL) {
        fin = stdin;
    }
    else if ((fin = fopen(fnin, "r")) == NULL) {
        perror(fnin);
        exit(1);
    }

    if (fnout == NULL) {
        fout = stdout;
#ifdef WIN32
        _setmode(_fileno(stdout), _O_BINARY);
#endif
    }
    else if ((fout = fopen(fnout, "wb")) == NULL) {
        perror(fnout);
        exit(1);
    }

    col = 0;                                        // we are starting between cards, expect start as first data line

    while (fgets(str, sizeof(str), fin) != NULL && ! failed) {
        alltrim(str);                               // trim leading/trailing blanks (including newline)
        lineno++;                                   // count input line

        if (*str == ';' || *str == '#'|| *str == '*' || ! *str)
            continue;                               // ignore comment or blank line

        if (strnicmp(str, "start", 5) == 0) {       // start marks new card, proceed to column 1 (strnicmp so trailing comment is ignored)
            if (col == 0)
                col = 1;
            else {
                fprintf(stderr, "\"start\" encountered where column %d was expected, at line %d\n", lineno);
                failed = TRUE;
            }
        }
        else if (strnicmp(str, "end", 3) == 0) {    // end is expected as 81'st data line
            if (col == 81) {
                fxwrite(buf, 2, 80, fout);          // write binary card image to output file
                ncards++;                           // increment card count
                col = 0;                            // reset, expect start next
            }
            else {
                fprintf(stderr, "\"end\" encountered where ");

                if (col == 0)
                    fprintf(stderr, "\"start\"");
                else
                    fprintf(stderr, "column %d", col);

                fprintf(stderr, " was expected, at line %d\n", lineno);
                failed = TRUE;
            }
        }
        else if (BETWEEN(col, 1, 80)) {             // for column 1 to 80, we expect a data line
            if (strnicmp(str, "blank", 5) == 0) {   // blank indicates an unpunched column
                buf[col-1] = 0;
                col++;
            }
            else {
                punches = 0;                        // prepare to parse a data line. Punches is output binary value for column

                v = 0;                              // v is current punch number
                gotnum = FALSE;                     // gotnum indicates we've seen a punch number

                for (c = str; ! failed; c++) {
                    if (BETWEEN(*c, '0', '9')) {    // this is a digit, accumulate into current punch number
                        v = v*10 + *c - '0';
                        gotnum = TRUE;              // note that we've seen a value
                    }
                    else if (*c == '-' || *c == '\0') {                 // at - separator or at end of string
                        if (gotnum && BETWEEN(v, 0, 12) && v != 10)
                            punches |= punchval[v];                     // add correct bit to column binary value
                        else {                                          // error if number not seen or punch number not 0..9, 11, or 12
                            fprintf(stderr, "Invalid punch value %d at line %d\n", v, lineno);
                            failed = TRUE;
                            break;
                        }

                        if (*c == '\0') {           // at end of string store value and advance column count
                            buf[col-1] = punches;
                            col++;
                            break;
                        }
                        else {
                            v = 0;                  // at separator, reset for next punch value
                            gotnum = FALSE;
                        }
                    }
                    else if (*c == '#' || *c == ';' || *c == '*') {
                        break;                      // terminate line parsing at comment character
                    }
                    else {                          // invalid character
                        fprintf(stderr, "Unexpected character '%c' at line %d\n", *c, lineno);
                        failed = TRUE;
                        break;
                    }
                }
            }
        }
        else {                                      // we expected start or end when not expecting column data
            fprintf(stderr, "\"%s\" encountered where \"%s\" was expected, at line %d\n", str,
                (col == 0) ? "start" : "end", lineno);
            failed = TRUE;
        }
    }

    fclose(fin);
    fclose(fout);
}

void toascii (char *fnin, char *fnout)
{
    FILE *fin, *fout;
    unsigned short buf[80], mask;
    int nread, col, row;
    BOOL first;
    static char *punchname[] = {"12", "11", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"};

    if (fnin == NULL) {
        fin = stdin;                                    // no input file named, read from stdin
#ifdef WIN32
        _setmode(_fileno(stdin), _O_BINARY);            // (on Windows, must set binary mode)
#endif
    }
    else if ((fin = fopen(fnin, "rb")) == NULL) {       // open named input file
        perror(fnin);
        exit(1);
    }

    if (fnout == NULL) {                                // no output file named, write to stdout
        fout = stdout;
    }
    else if ((fout = fopen(fnout, "wb")) == NULL) {     // open named output file
        perror(fnout);
        exit(1);
    }
                                                        // write comment with input file name
    fprintf(fout, "* converted from %s\n", (fnin == NULL) ? "<stdin>" : fnin);

    while ((nread = fxread(buf, 2, 80, fin)) == 80) {   // pull cards from binary file
        ncards++;                                       // increment card count
        fprintf(fout, "**** card %d\nstart\n", ncards); // write comment with card number and start statement

        for (col = 0; col < 80; col++) {                // dump 80 columns
            if (buf[col] == 0) {
                fprintf(fout, "blank\n");               // no punches this column
            }
            else if (buf[col] & 0x000F) {               // if low bits are set it is not a valid IBM1130 card image
                fprintf(stderr, "Input file is not an IBM 1130 card image, low bits set found at card image %d\n", ncards);
                failed = TRUE;
                break;
            }
            else {
                first = TRUE;                           // scan the 12 punch bits
                for (mask = 0x8000, row = 0; row < 12; row++, mask >>= 1) {
                    if (buf[col] & mask) {              // output name of punch row for each bit set (12, 10, 0, ..., 9)
                        fprintf(fout, "%s%s", first ? "" : "-", punchname[row]);
                        first = FALSE;                  // next punch will need a hyphen
                    }
                }
                putc('\n', fout);
            }
        }

        fprintf(fout, "end\n");
    }

    if (nread != 0) {                           // oops, file wasn't a multiple of 160 bytes in length
        fprintf(stderr, "Input file invalid or contained a partial card image\n");
        failed = TRUE;
    }

    fclose(fin);
    fclose(fout);
}

void bail (char *msg)
{
    fprintf(stderr, "%s\n", msg);
    exit(1);
}