/* bas.c - API for writing standalone programs that deal with
	tokenized Atari 8-bit BASIC program. */

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

#include "bas.h"

/* caller's parse_args() should set this for the -v option */
int verbose = 0;

/* hex constants work the same way in Turbo BASIC and BASIC XL/XE: they're
   exactly like numeric constants, 6-byte BCD FP, but introduced with
   OP_HEXCONST instead of OP_NUMCONST.
   If allow_hex_const is set, on_exp_token will be called for OP_HEXCONST.
	Otherwise, a warning will be printed, on_exp_token will NOT be called,
   but the constant is skipped the same as a regular numeric, to keep
   in sync with the token stream. */
int allow_hex_const = 0;

/* BASIC/A+ uses the same cmd tokens for REM and DATA that BASIC does,
   but not for the ERROR- token. Unfortunately bas.c needs to know it's
   an A+ program so it can handle this token correctly. */
int aplus_errtok_hack = 0;

/* BASIC XL token 0x5a is followed by a single "subtoken", this skips it. */
int bxl_exttok_hack = 0;

unsigned short lomem;
unsigned short vntp;
unsigned short vntd;
unsigned short vvtp;
unsigned short stmtab;
unsigned short stmcur;
unsigned short starp;
unsigned short codestart;
unsigned short code_end;
unsigned short vnstart;
unsigned short vvstart;
int filelen;
const char *self;
unsigned char program[BUFSIZE];
FILE *input_file;
FILE *output_file;
char *output_filename = NULL;

void die(const char *msg) {
	fprintf(stderr, "%s: %s\n", self, msg);
	exit(1);
}

void parse_general_args(int argc, char **argv, void (*helpfunc)()) {
	if(argc < 2) {
		(*helpfunc)();
		exit(1);
	}

	if(strcmp(argv[1], "--help") == 0) {
		(*helpfunc)();
		exit(0);
	}

	if(strcmp(argv[1], "--version") == 0) {
		printf("%s %s\n", self, VERSION);
		exit(0);
	}
}

/* read entire file into memory */
void readfile(void) {
	filelen = fread(program, 1, BUFSIZE - 1, input_file);
	if(verbose) fprintf(stderr, "Read %d bytes.\n", filelen);
	if(!feof(input_file))
		fprintf(stderr, "Warning: file is >64KB, way too big for a BASIC program.\n");
	else if(filelen > MAX_PROG_SIZE)
		fprintf(stderr, "Warning: file is %d bytes, suspiciously large for a BASIC program.\n", filelen);
	fclose(input_file);
	input_file = NULL; /* so caller can tell it's closed */
	if(filelen < MIN_PROG_SIZE)
		die("File too short to be a BASIC program (truncated?)\n");
}

int writefile(void) {
	int outbytes;

	outbytes = fwrite(program, 1, filelen, output_file);
	fclose(output_file);
	output_file = NULL; /* so caller can tell it's closed */
	if(verbose) fprintf(stderr, "Wrote %d bytes.\n", outbytes);
	return outbytes;
}

/* get a 16-bit value from the file, in 6502 LSB/MSB order. */
unsigned short getword(int addr) {
	return program[addr] | (program[addr + 1] << 8);
}

void setword(int addr, int value) {
	program[addr] = value & 0xff;
	program[addr + 1] = value >> 8;
}

void dump_header_vars(void) {
	fprintf(stderr, "LOMEM  $%04x    VNTP $%04x   VNTD $%04x  VVTP $%04x\n", lomem, vntp, vntd, vvtp);
	fprintf(stderr, "STMTAB $%04x  STMCUR $%04x  STARP $%04x\n", stmtab, stmcur, starp);
	fprintf(stderr, "vnstart $%04x, vvstart $%04x, codestart $%04x, code_end $%04x\n", vnstart, vvstart, codestart, code_end);
}

void parse_header(void) {
	int vntp_offset;

	lomem = getword(0);
	vntp = getword(2);
	vntd = getword(4);
	vvtp = getword(6);
	stmtab = getword(8);
	stmcur = getword(10);
	starp = getword(12);

	codestart = stmtab - TBL_OFFSET - (vntp - 256);
	vnstart = vntp - TBL_OFFSET;
	vvstart = vvtp - TBL_OFFSET;
	code_end = starp - TBL_OFFSET;

	if(vnstart > 0x0e) {
		if(verbose)
			fprintf(stderr, "VNTP is $%02x (not ($100), adjusting pointers.\n", vntp);
		vntp_offset = vnstart - 0x0e;
		vnstart -= vntp_offset;
		vvstart -= vntp_offset;
	}

	if(filelen < code_end) {
		fprintf(stderr, "Warning: file is truncated: %d bytes, should be %d.\n", filelen, code_end);
	}

	if(verbose) dump_header_vars();

	/* these checks are actually kind of conservative. */
	if(lomem) die("Not an Atari BASIC program (no $0000 signature).");
	if(vntp < 0x100) die("Not an Atari BASIC program (invalid VNTP).");
	if(vvtp < vntd) die("Not an Atari BASIC program (invalid VVTP).");
	if(starp < vvtp) die("Not an Atari BASIC program (invalid STARP).");
}

void update_header(void) {
	setword(0, lomem);
	setword(2, vntp);
	setword(4, vntd);
	setword(6, vvtp);
	setword(8, stmtab);
	setword(10, stmcur);
	setword(12, starp);
}

/* sometimes the variable name table isn't large enough to hold
	the generated variable names. move_code() makes more space,
	by moving the rest of the program (including the variable value
	table) up in memory. */
void move_code(int offset) {
	unsigned char *dest = program + vvstart + offset;

	if(dest < program || ((filelen + offset) > (BUFSIZE - 1))) {
		die("Attempt to move memory out of range; corrupt header bytes?\n");
	}

	memmove(dest, program + vvstart, filelen);

	vntd += offset;
	vvtp += offset;
	stmtab += offset;
	stmcur += offset;
	starp += offset;
	filelen += offset;
	update_header();
	parse_header();
}

void adjust_vntable_size(int oldsize, int newsize) {
	int move_by;
	if(oldsize != newsize) {
		move_by = newsize - oldsize;
		if(verbose) fprintf(stderr,
				"Need %d bytes for vntable, have %d, moving VVTP by %d to $%04x.\n",
				newsize, oldsize, move_by, vvtp + move_by);
		move_code(move_by);
	}
}

unsigned char get_vartype(unsigned char tok) {
	return program[vvstart + (tok & 0x7f) * 8] >> 6;
}

/* return true if the variable name table is OK */
int vntable_ok(void) {
	int vp, bad;

	if(vntp == vntd) {
		if(verbose) fprintf(stderr, "No variables.\n");
		return 1;
	}

	/* first pass: bad = 1 if all the bytes in the table have the same
		value, no matter what it is. */
	vp = vnstart + 1;

	/* don't do this check if the table is only one byte long! */
	if(vp < vvstart - 1) {
		bad = 1;
		while(vp < vvstart - 1) {
			if(program[vp] != program[vnstart]) {
				bad = 0;
				break;
			}
			vp++;
		}
		if(bad) return 0;
	}

	/* 2nd pass: bad = 1 if there's any invalid character in the table. */
	vp = vnstart;
	while(vp < vvstart) {
		unsigned char c = program[vp];

		/* allow for (but don't require) dummy byte at VNTD. used to just
		   quit when we hit 0, but 0 might be part of a scrambled table. */
		if(c == 0 && vp == (vvstart - 1)) break;

		vp++;

		/* inverse $ or ( is OK */
		if(c == 0xa4 || c == 0xa8) continue;

		/* numbers and letters are allowed, inverse or normal. */
		c &= 0x7f;
		if(c == '_') {
			fprintf(stderr, "%s: Underscore in variable name; Turbo or BASIC XE?\n", self);
			continue;
		}
		if(c >= 0x30 && c <= 0x39) continue;
		if(c >= 0x41 && c <= 0x5a) continue;

		bad++;
		break;
	}

	return !bad;
}

void invalid_args(const char *arg) {
	fprintf(stderr, "%s: Invalid argument '%s'.\n\n", self, arg);
	exit(1);
}

FILE *open_file(const char *name, const char *mode) {
	FILE *fp;
	if(!(fp = fopen(name, mode))) {
		perror(name);
		exit(1);
	}
	return fp;
}

void open_input(const char *name) {
	if(!name || strcmp(name, "-") == 0) {
		if(isatty(fileno(stdin))) {
			die("Can't read binary data from the terminal.");
		}
		if(freopen(NULL, "rb", stdin)) {
			input_file = stdin;
			return;
		} else {
			perror("stdin");
			exit(1);
		}
	}

	input_file = open_file(name, "rb");
}

void open_output(const char *name) {
	if(!name || (strcmp(name, "-") == 0)) {
		if(isatty(fileno(stdout))) {
			die("Refusing to write binary data to the terminal.");
		}
		if(freopen(NULL, "wb", stdout)) {
			output_file = stdout;
			return;
		} else {
			perror("stdout");
			exit(1);
		}
	}
	output_file = open_file(name, "wb");
}

void set_self(const char *argv0) {
	char *p;

	self = argv0;
	p = strrchr(self, '/');
	if(p) self = p + 1;
}

/* callbacks */
CALLBACK_PTR(on_start_line);
CALLBACK_PTR(on_bad_line_length);
CALLBACK_PTR(on_end_line);
CALLBACK_PTR(on_start_stmt);
CALLBACK_PTR(on_end_stmt);
CALLBACK_PTR(on_cmd_token);
CALLBACK_PTR(on_text);
CALLBACK_PTR(on_exp_token);
CALLBACK_PTR(on_var_token);
CALLBACK_PTR(on_string_const);
CALLBACK_PTR(on_num_const);
CALLBACK_PTR(on_trailing_garbage);

#define CALL(x) if(x) (*x)(lineno, pos, program[pos], end)

void walk_code(unsigned int startlineno, unsigned int endlineno) {
	int linepos, nextpos, offset, soffset, lineno = -1, tmpno, pos, end, tok;

	linepos = codestart;
	while(linepos < filelen) { /* loop over lines */
		tmpno = getword(linepos);
		if(tmpno <= lineno) {
			fprintf(stderr, "Warning: line number %d at offset $%04x is <= previous line number %d.\n",
					tmpno, linepos, lineno);
		}
		lineno = tmpno;
		offset = program[linepos + 2];
		nextpos = linepos + offset;
		if(nextpos > filelen) {
			fprintf(stderr, "Warning: program truncated in the middle of line %d.\n", lineno);
			return;
		}

		end = nextpos;
		pos = linepos;

		if(offset < 5) {
			/* actually, real Atari BASIC's minimum offset is 6. however, if you use
			   the "--" (line of dashes, command token 0x54) in Turbo BASIC XL, you
			   get an offset of 5, because there's no end-of-line after it.
			   it seems better to accomodate Turbo here. */
			CALL(on_bad_line_length);
			offset = program[linepos + 2]; /* on_bad_line_length fixed it (we hope) */
			if(offset < 5)
				die("Fatal: Program is code-protected; unprotect it first.");
		}

		if(lineno < startlineno) {
			linepos = nextpos;
			continue;
		}

		if(lineno > endlineno) break;

		CALL(on_start_line);

		pos = linepos + 3;
		while(pos < nextpos) { /* loop over statements within a line */
			soffset = program[pos];
			if(!soffset) {
				fprintf(stderr, "Fatal: next-statement offset is 0 at line %d, pos %04x\n", lineno, pos);
				exit(1);
			}
			end = linepos + soffset;
			CALL(on_start_stmt);

			while(pos < end) {  /* loop over tokens within a statement */
				pos++;
				CALL(on_cmd_token);

				tok = program[pos];
				if((tok == CMD_REM) || (tok == CMD_DATA) || /* same in A+ */
						(aplus_errtok_hack && tok == 0x53) || /* A+'s ERROR- */
						(!aplus_errtok_hack && tok == CMD_ERROR))
				{
					pos++;
					CALL(on_text);
					pos = end;
				} else if(bxl_exttok_hack && tok == 0x5a) {
					pos += 2; /* skip subtoken */
				} else {
					pos++;
				}

				while(pos < end) {     /* loop over operators */
					tok = program[pos];
					switch(tok) {
						case 0: /* Turbo variables numbered >= $80 */
							CALL(on_exp_token);
							/* on_exp_token callback better know what to do with $00! */
							pos += 2;
							break;
						case OP_NUMCONST:
							CALL(on_exp_token);
							pos++;
							CALL(on_num_const);
							pos += 6;
							break;
						case OP_HEXCONST:
							if(allow_hex_const) {
								CALL(on_exp_token);
							} else {
								fprintf(stderr, "%s: found Turbo/BXL/BXE hex constant at line %d, skipping\n", self, lineno);
							}
							pos++;
							if(allow_hex_const) {
								CALL(on_num_const);
							}
							pos += 6;
							break;
						case OP_STRCONST:
							CALL(on_exp_token);
							pos++;
							CALL(on_string_const);
							pos += program[pos] + 1;
							break;
						default:
							if(tok & 0x80) {
								CALL(on_var_token);
							} else {
								CALL(on_exp_token);
							}
							pos++;
							break;
					}
				}
				CALL(on_end_stmt);
			}
		}

		CALL(on_end_line);

		linepos = nextpos;
		if(lineno == 32768) break;
	}

	if(endlineno == 32768 && linepos < filelen) {
		if(verbose)
			fprintf(stderr, "%s: Trailing garbage at EOF, %d bytes.\n", self, filelen - linepos);
		CALL(on_trailing_garbage);
	}
}
