/*
 * oct2rim v3.0 -- Convert an OCT file as output by 'rimmer -o' to RIM format.
 *
 * Part of the  B L I N K Y - 1  project.
 *
 * Mike Hill    (v1.0)       18-Feb-2025.
 * Mike Hill    (v2.0)       31-Mar-2025.
 * Mike Hill    (v3.0)       14-Oct-2025.
 *
 */

/*      VERSION "v1.0"   -- Initial version                            */
/*      VERSION "v2.0"   -- Add automated title-line parsing           */
#define VERSION "v3.0"   /* Add ',' after address and no start address */

/*****************************************************************************/

/*
 * Description:
 *
 *    oct2bin is designed to create a PDP-1 RIM file from a text file in octal
 *    format.  'rimmer -o' creates such files from a PDP-1 paper-tape image.
 *    We now also support 'rimmer -O' format (with ',' for contiguous code).
 *
 *    'rimmer -o' (or '-O') creates files which can be assembled by macro1.
 *    oct2rim will also process files which CANNOT be assembled by macro1.
 *
 * Input (.oct) file format:
 *
 *    Any white space at the beginning of the file will ignored
 *    If the first character in the file is alphabetic, it acts like '/'.
 *    The initial 'address' is 0004 if not changed by '/' (below).
 *    Any contiguous string of octal digits will be treated as 'data'.
 *    If 'data' is followed directly by '/' it will be treated as 'address'.
 *    If '/' is not preceded by 'data' the rest of the line will be ignored.
 *    If 'data' is followed directly by ',' it must match the current address.
 *    Any other character or CR/LF will punch the address/data word-pair.
 *    After a word-pair is punched, the address will be incremented.
 *    If 's' or 'S' is seen, the 'data' following it is treated as 'start'.
 *    If no 'data' follows 'start' then no start address will be punched.
 *    When 'start' has been defined, it will be punched as the start address.
 *    Following 'start', the rest of the input file will be ignored.
 *
 * Usage:
 *
 *    oct2rim                    Show the usage text
 *    oct2rim -v                 Show the version number and usage text
 *    oct2rim --version          Show the version number and usage text
 *    oct2rim octfile -d         Debug the RIM output as text (to stdout)
 *    oct2rim octfile --debug    Debug the RIM output as text (to stdout)
 *    oct2rim octfile rimfile    Convert the <octfile> to the <rimfile>
 *
 * Messages:
 *
 *    <filename> could not be opened: <reason>
 *        Correct the <reason> for the named file
 *
 *    Address overflowed with data: xxxxxx
 *        Addresses may not exceed 7777 and data xxxxxx tried to
 *        Search for 'xxxxxx' in the <octfile> after address 7777
 *        Use 'oct2rim octfile --debug' to see where the error might lie
 *
 *    Invalid address: xxxxxx > 7777
 *        Search for 'xxxxxx/' in the <octfile>
 *        Valid addresses are from 0000-7777
 *
 *    Invalid address: 'start' xxxxxx > 7777
 *        Search for 'xxxxxx/' in the <octfile>
 *        Valid addresses are from 0000-7777
 *
 *    Invalid data: xxxxxxx > 777777
 *        Search for 'xxxxxxx' in the <octfile>
 *        Valid data is from 000000-777777
 *
 *    No 'start' address found
 *        No address followed the word 'start'
 *        Of course, this may indicate that no start address was desired
 *        Otherwise, look for the word 'start' in <octfile> and ensure
 *        it is followed by a valid address (0000-7777)
 *
 *    No 'start' statement found
 *        The word 'start' was not found in <octfile>
 *        This may indicate that <octfile> is not in the correct format
 *        Add the word 'start' at the end of the file and (optionally)
 *        follow it with a valid address (0000-7777)
 *
 *    Output to <rimfile> could not be completed
 *        This will be shown if the <rimfile> does not contain valid data
 *        Correct the problem and retry the command
 *
 *    Punched <count> word-pairs to <rimfile>
 *        This is shown if the <rimfile> is completed BUT does not contain
 *        a start address.  Verify that the <count> matches what you expect
 *
 *    Punched <count> word-pairs to <rimfile>, start = <startaddress>
 *        This is only shown if the <rimfile> is successfully completed
 *        Verify that the <count> and <startaddress> match what you expect
 *
 *    Saw 'xxxxxx,' but expected address xxxxxx
 *        This will be shown ONCE if the current (expected) address does not
 *        match the desired address (to the left of ',') NOW any commas seen
 *        following this message will be treated like any other delimiter
 *
 * Example (test.oct):
 *
        /TITLE LINE.  The first line in the file (the macro1 title line).
        /If the first line starts with A-Z or a-z it will be a comment.
        /All other comment lines must start with a '/'.

                760400                          / 'hlt'   stored  at  0004
                600000                          / 'jmp 0' stored  at  0005

        0000/   000000  000001  000002  000003  / 4 words stored from 0000-0003
        0010/10.11 12.13 14.15 16.17.           / 8 words stored from 0010-0017
	0020,20					/ 1 word  stored at   0020
	0021,					/ Nothing stored at   0021

        0100/           / Set  address   to 0100
                000100  / 000100  stored at 0100
                000101  / 000101  stored at 0101
                000102  / 000102  stored at 0102

        0200/           / Nothing stored at 0200

        noinput         / Does not contain 's' so will be ignored by oct2rim
        start           / The 'start' address need not be on the same line
                0005    / Start address is 0005 (START 0005 would also work)

        / The rest of the file will be ignored

        1234/   654321    / This will NOT be punched ('start' ends processing)
        77777/  7777777   / Nor will this (which would normally fail)
*/

/*****************************************************************************/

/*
 * This software was built and tested using the Microsoft (R) 32-bit
 *     C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86.
 *
 * Qualifiers used to compile it were:
 *     /D "WIN32" /TC /W4
 *
 * This source file (oct2rim.c) contains 381 lines and 13252 characters.
 */

/* Ignore security warnings in Microsoft C */

#ifdef _MSC_VER
#define _CRT_SECURE_NO_WARNINGS 1
#endif

/* Includes */

#include <ctype.h>       /* Only required for isalpha() & isspace() */
#include <errno.h>       /* Optional for errno (included by stdlib) */
#include <stdio.h>       /* Required for all I/O                    */
#include <stdlib.h>      /* Required for abort() and EXIT_xxxxxxx   */
#include <string.h>      /* Only required for strerror()            */

/* Defines */

#define INITADDR 00004   /* Default initial address until '/' seen  */

#define DIO 0320000      /* PDP-1 'dio' opcode                      */
#define JMP 0600000      /* PDP-1 'jmp' opcode                      */

/* Global variables */

FILE *octfile;           /* Input file in 'rimmer -o|-O' format     */
FILE *rimfile = NULL;    /* Output file in PDP-1 RIM format         */

int   chkcommas = 1;     /* 1=Check 'comma' addresses, 0=Don't      */
int   wordpairs = 0;     /* Number of word-pairs written            */
int   startaddr = -1;    /* Start address (when [if] we see it)     */

/*****************************************************************************/

/* ppa() -- Punch a single ASCII character */

void ppa(int data) {
    if (fputc(data /* & 0377 */, rimfile) == EOF)
        abort();
}

/*****************************************************************************/

/* ppb() -- Punch a binary 18-bit word */

void ppb(int data) {
    static int comma = 0;

    if (rimfile) {
        ppa(0200 | ((data >> 12) & 077));
        ppa(0200 | ((data >> 6)  & 077));
        ppa(0200 | ((data)       & 077));
    } else {
        comma = !comma;
        if (comma)
            comma = (data & 0770000) == DIO;
        fprintf(stdout, "%6.6o%c", data, (comma) ? ',' : '\n');
    }
}

/*****************************************************************************/

/* oct2rim() -- Convert the whole input file to PDP-1 RIM format */

int oct2rim(void) {
    int addr   = INITADDR;          /* Next address   to be written (init) */
    int data   = -1;                /* Next data word to be written (none) */
    int c      = fgetc(octfile);    /* Current input character (< 0 = EOF) */
    int status = EXIT_SUCCESS;      /* Assume we will be successful        */

    /* Skip any white space at the beginning of the octal file */

    while (isspace(c))
        c = fgetc(octfile);

    /* If the first character in the file is A-Z or a-z make it a comment */

    if (isalpha(c))
        c = '/';

    /* Parse the rest of the file until a 'start' or EOF is seen */

    for (;;) {
        if (c >= '0' && c <= '7') {

            /* Convert a string of digits to octal */

            if (data < 0) {
                data = (c - '0');
            } else {
                data = data * 010 + (c - '0');
                if (data > 0777777) {
                    fprintf(stderr, "Invalid data: %o > 777777\n", data);
                    return EXIT_FAILURE;
                }
            }
        } else {
            if (c == ',' && chkcommas && data >= 0) {

                /* If we see a ',' directly preceded by data: check address */

                if (data == addr) {
                    c = '/';    /* Pretend this is a '/' (to skip the ',') */
                } else {
                    fprintf(stderr, "Saw '%o,' but expected address %4.4o\n",
                            data, addr);
                    chkcommas = 0;
                    status = EXIT_FAILURE;
                /* return EXIT_FAILURE;  */
                }
            }

            if (c == '/') {
                if (data >= 0) {

                    /* We assume '/' directly preceded by data is an address */

                    addr = data;
                    if (addr > 07777) {
                        fprintf(stderr, "Invalid address: %s%o > 7777\n",
                                (startaddr > 07777) ? "'start' " : "", addr);
                        return EXIT_FAILURE;
                    }
                    if (startaddr >= 0) {
                        startaddr = addr;
                        break;
                    }
                    data = -1;
                } else {

                    /* When '/' is NOT preceded by data skip to end of line */

                    do {
                        c = fgetc(octfile);
                    } while ((c >= 0) && (c != '\n'));
                }
            }

            if (data >= 0) {

                /* If we expect a 'start' address, prepare to punch it */

                if (startaddr >= 0) {
                    c = '/';
                    continue;
                }

                /* Punch the data word-pair */

                if (addr > 07777) {
                    fprintf(stderr,
                            "Address overflowed with data: %6.6o\n", data);
                    return EXIT_FAILURE;
                }
                ppb(DIO | addr++);
                ppb(data);
                wordpairs++;
                data = -1;
            } else {

                /* If we see an 's', assume 'start' address will follow */

                if (c == 's' || c == 'S')
                   startaddr = 010000;    /* Invalid but >= 0 */
            }
            if (c < 0)
                break;    /* EOF seen */
        }
        c = fgetc(octfile);
    }

    if (startaddr < 0) {
        fprintf(stderr, "No 'start' statement found\n");
        return EXIT_FAILURE;
    }

    if (startaddr > 07777) {
        fprintf(stderr, "No 'start' address found\n");
        if (wordpairs == 0)
            return EXIT_FAILURE;
    }

    ppb(JMP | startaddr);    /* 'jmp' to the 'start' address */
    return status;
}

/*****************************************************************************/

/* main() -- Main program */

int main(int argc, char *argv[]) {
    int status;    /* Exit status */

    if (argc != 3) {
        if (argc == 2)    /* Display the version number for one parameter */
            fprintf(stderr, "oct2rim %s ...\n", VERSION);
        fprintf(stderr, "Usage: %s octfile rimfile\n", argv[0]);
        fprintf(stderr, "       %s octfile --debug\n", argv[0]);
        return EXIT_FAILURE;
    }

    octfile = fopen(argv[1], "r");
    if (octfile == NULL) {
        fprintf(stderr, "%s could not be opened: %s\n",
                        argv[1], strerror(errno));
        return EXIT_FAILURE;
    }

    if (*argv[2] != '-') {
        rimfile = fopen(argv[2], "wb");
        if (rimfile == NULL) {
            fprintf(stderr, "%s could not be opened: %s\n",
                            argv[2], strerror(errno));
            return EXIT_FAILURE;
        }
    }

    status = oct2rim();    /* Do the actual conversion */

    fclose(octfile);       /* Don't care about the status */

    if (rimfile)
        if (fclose(rimfile) == EOF)
            status = EXIT_FAILURE;

    if (wordpairs > 0 || (startaddr >= 0 && startaddr <= 07777)) {
        fprintf(stderr, "Punched %d word-pairs to %s", wordpairs, argv[2]);
        if (startaddr >= 0 && startaddr <= 07777)
            fprintf(stderr, ", start = %4.4o", startaddr);
        fprintf(stderr, "\n");
    }

    if (status != EXIT_SUCCESS)
        fprintf(stderr, "Output to %s could not be completed\n", argv[2]);

    return status;
}
