/*
 * This file is part of Commodore 64 emulator
 *      and Program Development System.
 *
 *   Copyright (C) 1993-1995,1996, Jouko Valta
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *
 * This program converts your SEQ files as well as expands tokenized
 * C64/128 BASIC programs into 7-bit ASCII text. Unprintable characters
 * can be shown as hexadecimal codes in parenthesis, via `-c' option.
 * It is also possible to convert programs from ascii listings into
 * tokenized basic v2.0, v3.5, v4.0, v7.0 or simon's basic programs. This
 * program also replaces certain control code names with the actual codes.
 *
 * A list of Toolkit Basic (published by Compute! Books) token codes
 * would be greatly appreciated. (-:
 *
 * Runs on UNIX or Atari ST.
 *
 * In shell:
 *  tr '\015A-Za-z\301-\332\\\|\[\{\]\}' '\012a-zA-ZA-Z\|\\\{\[\}\]'
 * or
 *  tr '\015A-Za-z\\\|\[\{\]\}' '\012a-zA-Z\|\\\{\[\}\]'
 *
 * Written by
 *   Jouko Valta <jopi@stekt.oulu.fi>
 *
 * With additional changes by
 *   Ettore Perazzoli <ettore@comm2000.it>
 *   Spiro Trikaliotis <spiro.trikaliotis@gmx.de>
 *
 * Support for Final Cartridge III extensions to c64 2.0 basic
 *   Matti 'ccr' Hämäläinen <ccr@tnsp.org>
 *
 * Support for many of the other extensions by
 *   Marco van den Heuvel <blackystardust68@yahoo.com>
 *
 * Modified for use with ct-BASIC (derived from EhBASIC by Lee Davidson) by
 *   C. Meyer, <cm@ctmagazin.de>, 20.05.2009
 *   runs on Win32
 */

/* #define DEBUG */

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

//#include "types.h"

#define PETCATVERSION   2.17
#define PETCATLEVEL     1

/* Limits */


#define NUM_COMM        116     /* common for all versions 2.0 ... 10.0 */
#define MAX_COMM        0xFE    /* common for all versions */

#define KW_NONE         0xFE    /* flag unused token */

#define CLARIF_LP       '#'     /* control code left delimiter */
#define CLARIF_RP       '#'     /* control code right delimiter */

#define CLARIF_LP_ST    "#"     /* control code left delimiter, "string version" */
#define CLARIF_RP_ST    "#"     /* control code right delimiter, "string version" */

/* ------------------------------------------------------------------------- */

 /*
  * Printer's control code symbols
  */

/* all numeric codes */

/* 0x00 - 0x1f */
static const char *ctrl1[] = {
    "",  "", "", "", "", "WHT", "", "",
    "DISH", "ENSH", "\N", "", "\F", "\N", "SWLC", "",
    "",  "DOWN", "RVON", "HOME", "DEL", "", "", "",
    "",  "",  "", "ESC", "RED", "RGHT", "GRN", "BLU"
};
/* 0x80 - 0x9f */
static const char *ctrl2[] = {
    "", "ORNG",  "",  "",  "",  "F1",  "F3",  "F5",
    "F7",  "F2", "F4",   "F6",   "F8",  "SRET", "SWUC", "",
    "BLK",  "UP", "RVOF", "CLR",  "INST", "BRN",  "LRED", "GRY1",
    "GRY2", "LGRN", "LBLU", "GRY3", "PUR", "LEFT", "YEL", "CYN"
};

#define NUM_VERSIONS  1



/*
 * Two BASIC tokens which need some special care
 */
#define TOKEN_REM  (0x99 - 0x80)
#define TOKEN_DATA (0x83 - 0x80)


/*
 * EhBasic Keywords
Functions with (): 
SGN() INT() ABS() USR() FRE() POS() SQR() RND() LOG() EXP()
COS() SIN() TAN() ATN() PEEK() DEEK() SADD() LEN() STR$() VAL() ASC()      
UCASE$() LCASE$() CHR$() HEX$() BIN$()	BITTST() MAX() MIN()
VARPTR() LEFT$() RIGHT$() MID$()  
 */

const char *keyword[] = {
    /* EhBASIC Keywords */
    "END",   "FOR",  "NEXT", "DATA", "INPUT",  "DIM", "READ",
    "LET",  "DEC",  "GOTO", "RUN",  "IF", "RESTORE", "GOSUB", "COLOR",
    "CURSOR", "LABCMD", "LABCLR", "LABINP", "LABOUT", "LABREQ", "LABBUS",
    "MACRO", "RENUMBER", "RETURN", "REM",
    "PLOT",  "ON", "BANK", "INC",   
    "WAIT", "LOAD", "SAVE",  "DIR", "DEF", "POKE",
    "DOKE", "CALL", "DO", "LOOP", "PRINT", "FRAME", "LIST", "CLEAR", "NEW",
    "WIDTH", "GET", "SWAP", "BITSET", "BITCLR", 
    "TAB(", "TO",    "FN",  "SPC(", "THEN",
    "NOT",   "STEP", "UNTIL", "WHILE", "OFF", "+",  "-", "*", "/", "^", "AND",
    "EOR", "OR",  ">>", "<<", ">",  "=", "<", "SGN(", "INT(", "ABS(",  "USR(",
    "FRE(",   "POS(",  "SQR(",  "RND(",  "LOG(",   "EXP(", "COS(",  "SIN(",
    "TAN(",   "ATN(",  "PEEK(", "DEEK(", "SADD(", "LEN(",  "STR$(",  "VAL(", "ASC(", 
    "UCASE$(", "LCASE$(", "CHR$(", "HEX$(", "BIN$(", "BITTST(", "MAX(", "MIN(", "PI", "TWOPI",
    "VARPTR(",  "LEFT$(", "RIGHT$(", "MID$(", 
    "POSITION", "GRID",  "SIZE", "MOVE", "PIXEL", "LINE", "RECT",  "FILL"
};



/* ------------------------------------------------------------------------- */

static int parse_version(char *str);
static void list_keywords(int max);
static void pet_2_asc (int ctrls);
static void _p_toascii(int c, int ctrls);
static int p_expand(int version, int addr, int ctrls);
static void p_tokenize(int version, unsigned int addr, int ctrls);
static unsigned char sstrcmp(unsigned char *line, const char **wordlist,
                             int token, int maxitems);

/* ------------------------------------------------------------------------- */

static FILE *source, *dest;
static int kwlen;


/* ------------------------------------------------------------------------- */

int main(int argc, char **argv)
{
    char *progname, *outfilename = NULL;
    int c = 0;

    long offset = 0;
    int wr_mode = 0;         /* best defaults */
    int load_addr = 0, ctrls= -1, hdr = -1, show_words = 0;
    int fil = 0, outf = 0, overwrt = 0, textmode = 0;
    int flg = 0;                            /* files on stdin */


    /* Parse arguments */
    progname = argv[0];
    while (--argc && ((*++argv)[0] == '-')) {
        if (!strcmp(argv[0], "--")) {
            --argc;
            ++argv;
            break;
        }

        if (!strcmp(argv[0], "-l")) {           /* load address */
            if (argc > 1 && sscanf(argv[1], "%x", &load_addr) == 1) {
                --argc; ++argv;
            continue;
            }
            /* Fall to error */
        }

        if (!strcmp(argv[0], "-c")) {
            ctrls = 1;
            continue;
        } else {
            if (!strcmp(argv[0], "-nc")) {
                ctrls = 0;
                continue;
            }
        }
        if (!strcmp(argv[0], "-k")) {
            show_words = 1;
            continue;
        } 
        if (!strcmp(argv[0], "-h")) {
            hdr = 1;
            continue;
        } else if (!strcmp(argv[0], "-nh")) {
            hdr = 0;
            continue;
        } else if (!strcmp(argv[0], "-f")) {      /* force overwrite */
            ++overwrt;
            continue;
        }

        else if (!strcmp(argv[0], "-o")) {
            if (argc > 1) {
                outfilename = argv[1];
                ++outf;
                --argc; ++argv;
                continue;
            }
            fprintf (stderr, "\nOutput filename missing\n");
            /* Fall to error */
        }

        /* reading offset */
        if (!strcmp(argv[0], "-skip") || !strcmp(argv[0], "-offset")) {
            if (argc > 1 && sscanf(argv[1], "%lx", &offset) == 1)
                {
                    --argc; ++argv;
                    continue;
                }
            /* Fall to error */

        } else if (!strcmp(argv[0], "-text")) {   /* force text mode */
            ++textmode;
            continue;

        } else if (!strcmp(argv[0], "-help") ||
                 !strncmp(argv[0], "-v", 2)) {  /* version ID */
            fprintf(stdout,
                    "\n\t%s V%4.2f PL %d -- EhBASIC list/crunch utility.\n\tPart of ct-BASIC\n",
                    progname, (float)PETCATVERSION, PETCATLEVEL );

            /* Fall to error for Usage */



        }

        fprintf(stdout,
                "\nUsage: %7s  [-c | -nc]  [-h | -nh]  [-text | -<version> | -w<version>]"
                "\n\t\t[-skip <bytes>] [-l <hex>]  [--] [file list]\n\t\t[-k]\n",
                progname);

        fprintf(stdout, "\n"
            "   -help\tOutput this help screen here\n"
            "   -v\t\tSame as above\n"
            "   -c\t\tcontrols (interpret also control codes) <default if textmode>\n"
            "   -nc\t\tno controls (suppress control codes in printout)\n"
            "   \t\t<default if non-textmode>\n"
            "   -h\t\twrite header <default if output is stdout>\n"
            "   -nh\t\tno header <default if output is a file>\n"
            "   -skip <n>\tSkip <n> bytes in the beginning of input file. Ignored on P00.\n"
            "   -text\tForce text mode\n"
            "   -k\t\tlist all keywords for EhBASIC\n"
            "   -l\t\tSpecify load address for program (in hex, no loading chars!).\n"
            "   -o<name>\tSpecify the output file name\n"
            "   -f\t\tForce overwritten the output file\n"
            "   \t\tThe default depends on the BASIC version.\n");


        fprintf(stdout, "\tUsage examples:\n"
            "\tehbc -o outputfile.txt -- inputfile.bas\n"
            "\t\tConvert inputfile.bas to a text file in outputfile.txt\n"
            "\tehbc -o outputfile.bas -- inputfile.txt\n"
            "\t\tConvert inputfile.txt to a BAS file in outputfile.bas\n");
        exit(1);
    }


/*
 * Check parameters
 */

    if (argc)
        fil++;

    if (hdr==-1)
        hdr = outf ? 0 : 1;

    if (show_words) {
        list_keywords(NUM_COMM);
        
        return (0);
    }

    if (ctrls < 0)
        ctrls = (textmode ? 0 : 1);     /*default ON for prgs, OFF for text */


    if (!load_addr) {
            load_addr = 0x0800;
    }

    if (wr_mode) {
        fprintf (stderr, "\nLoad address %04x\n", load_addr);
        if ((load_addr & 255) != 0) {
            fprintf (stderr, "I'm afraid I cannot accept that.\n");
            exit(1);
        }

        fprintf(stderr, "Control code set: %s\n\n",
                (ctrls ? "enabled" : "disabled"));
    }


    /*
     * Loop all files
     */

    do {
        int  plen = 0;
        flg = 0;        /* stdin loop flag */


        if ((source = fopen(argv[0], "rb")) == NULL) {
            fprintf(stderr,
                    "\n%s: Can't open file %s\n", progname, argv[0]);
            exit(1);
        }


        if (!outf)
            dest = stdout;
        else {
            /*if (extension && *extension == '.') {
                sprintf(outfilename, "%s%s", argv[0], extension);
            }*/

            if ((dest = fopen(outfilename, "wb")) == NULL) {
                fprintf(stderr,
                        "\n%s: Can't open output file %s\n", progname, outfilename);
                exit(1);
            }
        }


        if (wr_mode) {
            p_tokenize(0, load_addr, ctrls);
        } else {
            if (hdr)
                fprintf(dest, "\n\n%s", (fil ? argv[0] : "<stdin>"),"\n");

            /*
             * Use TEXT mode if the offset doesn't equal BASIC load addresses
             * and the first bytes to be read do not contain load address.
             * Explicitly selected textmode overrules these conditions.
             */

            if (textmode || ((offset & 255) !=1 &&
                ((c = getc(source)) != EOF && ungetc(c, source) != EOF &&
                c && c != 1)) ) {
                	
                pet_2_asc(ctrls);
            }
            else {

                /* get load address */
                getc(source);
                load_addr =0x800;

                if (hdr)
                    fprintf(dest, "==%04x==\n", load_addr,"\n");

                if (p_expand(0, load_addr, ctrls)) {
                    fprintf(dest, "\n*** Machine language part skipped. ***\n");
                }
                else    /* End of BASIC on stdin. Is there more ? */
                    if (!fil && (c = getc(source)) != EOF &&
                        ungetc(c, source) != EOF && c) {
                        ++flg;
                        ++hdr;
                    }
            }

            if (hdr)
                fputc('\n', dest);
        }


        if (fil)
            fclose(source);
        if (outf)
            fclose(dest);

    } while (flg || (fil && --argc && ++argv));         /* next file */
    return(0);
}

/* ------------------------------------------------------------------------- */


static void list_keywords(int max)
{
    int n,m;

    printf("\nAvailable EhBASIC tokens\n\n");
    for (n = 0; n < max/4; n++) {
        for (m = 0; m < 4; m++) {
            printf("%3d %s   \t", n*4+m, keyword[n*4+m] /*, n | 0x80*/);
        }
    printf("\n");
    }
    printf("\n\n");
}

/* ------------------------------------------------------------------------- */

/*
 * Conversion Routines
 */

static void pet_2_asc(int ctrls)
{
    int c;

    while ((c = getc(source)) != EOF) {
        _p_toascii(c, ctrls);           /* convert character */
    }      /* line */
}


static void _p_toascii(int c, int ctrls)
{
    switch (c) {
      case 0x00:                          /* 00 for SEQ */
      case 0x0a:
      case 0x0d:
        fputc ('\n', dest);
        break;
      default:
        if (isprint(c))
           fputc(c, dest);
        else if (ctrls) {
           fprintf(dest, CLARIF_LP_ST "$%02x" CLARIF_RP_ST, c & 0xff);
          }  /* ctrls */
    }  /* switch */
}




/*
 * This routine starts from the beginning of Basic, and not from the
 * load address included on program files. That way it can list from
 * RAM dump.
 */

static int p_expand(int version, int addr, int ctrls)
{
    static char line[4];
    unsigned int c;
    int quote, spnum, directory = 0;
    int sysflg = 0;

    /*
     * It seems to be common mistake not to terminate BASIC properly
     * before the machine language part, so we don't check for the
     * low byte of line link here.
     * Line link and line number are read separately to leave possible
     * next file on stdin intact.
     */

    while ((fread(line, 1, 2, source) == 2) && (/*line[0] ||*/ line[1]) &&
           fread(line + 2, 1, 2, source) == 2) {
        quote = 0;
        fprintf(dest, " %4d ",
                (spnum = (line[2] & 0xff) + ((line[3] & 0xff) << 8)) );

        if (directory) {
            if (spnum >= 100)
                spnum = 0;
            else if (spnum >= 10)
                spnum = 1;
            else
                spnum = 2;
        }

        /* prevent list protection from terminating listing */

        while ((c = getc(source)) != EOF && !c);

        if (c == 0x12 && !line[2] && !line[3]) {  /* 00 00 12 22 */
            directory++;
        }

        do {
            if (c == 0x22)
                quote ^= c;


            /* basic 2.0, 7.0 & 10.0 and extensions */

            if (!quote && c > 0x7f) {
                if (c <= MAX_COMM) {
                    fprintf(dest, "%s", keyword[c & 0x7f]);

                    if (c == 0x9E) {
                        ++sysflg;
                    }
                    continue;
                }
 
                    fprintf(dest, "%s", keyword[c & 0x7f]);
            } /* quote */

            if (directory && spnum && c == 0x20) {
                spnum--;          /* eat spaces to adjust directory lines */
                continue;
            }

            _p_toascii((int)c, ctrls);  /* convert character */

        } while ((c = getc(source)) != EOF && c);
        fprintf(dest, "\n");

    }      /* line */

#ifdef DEBUG
    printf("\n c %02x  EOF %d  *line %d  sysflg %d\n",
           c, feof(source), *line, sysflg);
#endif

    return (!feof(source) && (*line | line[1]) && sysflg);
}

/*
03/09/2006 gpz:
- changed to output tokenized line into a second buffer instead of
  putting the tokens back in the input buffer. i wonder how this ever
  worked, control codes with a reasonable high repeatcount made
  previous versions of petcat segfault (or with luck, output trash)
- added error message when there is an unknown controlcode inside braces
*/
static void p_tokenize(int version, unsigned int addr, int ctrls)
{
    static char line[256];
    static char tokenizedline[256];
    unsigned char *p1, *p2, quote, c;
    unsigned char rem_data_mode, rem_data_endchar = '\0';
    int len = 0, match;
    unsigned int linum = 10;

    fprintf(dest, "%c%c", (addr & 255), ((addr >> 8) & 255));

    /* Copies from p2 to p1 */

    while((p2 = (unsigned char *)fgets(line, 255, source)) != NULL) {

	memset(tokenizedline, 0, 256);
	p1 = (unsigned char *)tokenizedline;

#ifndef GEMDOS
        if (sscanf(line, "%d%n", &linum, &len) == 1)
            p2 += len;
#else
        if (sscanf(line, "%d", &linum) == 1)
            while (isspace(*p2) || isdigit(*p2)) p2++;
#endif

#ifdef DEBUG
	fprintf(stderr,"line: %d\n",linum);
#endif
        quote = 0;
        rem_data_mode = 0;
        while (isspace(*p2)) p2++;

        while (*p2) {
            if (*p2 == 0x22) {
                quote ^= *p2;
            }
            if (*p2 == 0x0a || *p2 == 0x0d)
                break;

            match = 0;
            if (quote) {
                /*
                 * control code evaluation
                 * only strings that appear inside quotes are
                 * interpreted -- they should not be used elsewhere
                 */

                if (ctrls && (*p2 == CLARIF_LP)) {
                    unsigned char *p;
                    p = p2;

#ifdef DEBUG
	fprintf(stderr,"controlcode start: %c\n", *p2);
#endif

                    /* repetition count */
                    len = 1;
#ifndef GEMDOS
                    if (sscanf((char *)++p, "%d%n", &len, &kwlen) == 1) {
                        p += kwlen;
#else
                    if (sscanf(++p, "%d", &len) == 1) {
                        while (isspace(*p) || isdigit(*p)) p++;
#endif

#ifdef DEBUG
	fprintf(stderr,"controlcode repeat count: len:%d kwlen:%d\n", len,kwlen);
#endif

                    if (*p == ' ')
                            ++p;
                    }

                }
#ifdef DEBUG
/*	fprintf(stderr,"controlcode end\n"); */
#endif

            }
            else if (rem_data_mode) {
                /* if we have already encountered a REM or a DATA, 
                   simply copy the char */

                /* DO NOTHING! As we do not set "match", the if (!match) will be true,
                 * and this part will copy the char over to the new buffer */
            }
            else if (isalpha(*p2) || strchr("+-*/^>=<", *p2)) {


                /* Common Keywords
                 * Note:  ~ (pi) is tested later */


                if (!match) {
                    int max;

                    max = NUM_COMM;

                    if ((c = sstrcmp(p2, keyword, 0, max)) != KW_NONE) {

                        if (c != 0x4e) {  /* Skip prefix */
                            *p1++ = c | 0x80;
                            p2 += kwlen;
                            match++;

                            /* Check if the keyword is a REM or a DATA */
                            switch (c) {
                            case TOKEN_DATA:
                                rem_data_mode = 1;
                                rem_data_endchar = ':';
                                break;

                            case TOKEN_REM:
                                rem_data_mode = 1;
                                rem_data_endchar = '\0';
                                break;
                            }
                        }
                    }
                }

             } /* !quote */

            if (!match) {
                if ((*p2 >= 0x5b) && (*p2 <= 0x7e))
                    *p1++ = *p2 ^ 0x20;

                else if ((*p2 >= 'A') && (*p2 <= 'Z'))
                    *p1++ = *p2 | 0x80;

                else
                    *p1++ = *p2;

                /* check if the REM/DATA mode has to be stopped: */
                if (*p2 == rem_data_endchar)
                    rem_data_mode = 0;

                ++p2;
            } /* match */
        } /* while */

#ifdef DEBUG
	fprintf(stderr,"output line start: %s\n", line);
/*	fprintf(stderr,"output line petscii: %s\n", tokenizedline); */
#endif

        *p1 = 0;
        if ((len = strlen(tokenizedline) ) > 0) {
            addr += len + 5;
            fprintf(dest, "%c%c%c%c%s%c", addr & 255, (addr>>8) & 255,
                   linum & 255, (linum>>8) & 255, tokenizedline, '\0');

            linum += 2; /* auto line numbering by default */
        }

#ifdef DEBUG
	fprintf(stderr,"output line end\n");
#endif

    } /* while */

    fprintf(dest, "%c%c", 0, 0);        /* program end marker */
}


static unsigned char sstrcmp(unsigned char *line, const char **wordlist,
                            int token, int maxitems)
{
    int j;
    const char *p, *q;

    kwlen = 1;
    /* search for keyword */
    for (; token < maxitems; token++) {
        for (p = wordlist[token], q = (char *)line, j = 0;
             *p && *q && *p == *q; p++, q++, j++);

        /*fprintf (stderr,
                 "compare %s %s - %d %d\n", wordlist[token], line, j, kwlen);*/

        /* found an exact or abbreviated keyword
         */
        if (j && (!*p || (*p && (*p ^ *q) == 0x20 && j++)) ) {
            kwlen = j;
            /*fprintf (stderr, "found %s %2x\n", wordlist[token], token);*/
            return token;
        }
    } /* for */

    return (KW_NONE);
}

