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

#include <math.h>
#include <errno.h>

#include <sys/wait.h>

#include "bas.h"
#include "bcdfp.h"
#include "tokens.h"
#include "turbo_tokens.h"
#include "aplus_tokens.h"
#include "bxl_tokens.h"
#include "bxe_tokens.h"
#include "atables.h"
#include "whichbas.h"

#define B_ATARI SRET_ATARI
#define B_TURBO SRET_TURBO
#define B_APLUS SRET_APLUS
#define B_BXL   SRET_BXL
#define B_BXE   SRET_BXE

#define COLOR_FMT "\x1b[%d;3%dm"  /* 1st %d is 1 for bold, 2nd is color */

#define COLOR_OFF "\x1b[0m" /* clears inverse, too! */
#define INV_OFF   "\x1b[0m" /* clears color, too! */

/* These would be better to use, allow turning off color and inverse
   independently, but are not as widely supported by various terminals. */
/* #define COLOR_OFF "\x1b[39;49m" */
/* #define INV_OFF   "\x1b[27m"" */

#define INV_ON   "\x1b[7m"  /* goes all the way back to the original VT100 */
#define ULINE_ON "\x1b[4m"

#define MAG_INV  "{inv}"
#define MAG_NORM "{norm}"

/* colors. */
#define NO_COLOR -1
#define C_RED     1
#define C_GREEN   2
#define C_YELLOW  3
#define C_BLUE    4
#define C_PURPLE  5
#define C_CYAN    6
#define C_WHITE   7

#define ENV_OPTS "LISTBAS_OPTS"
#define ENV_MAX_ARGS 64
/* #define DEBUG_ENV */

/* output modes */
#define M_UTF8    0   /* default */
#define M_UTF8_I  1   /* -x */
#define M_ATASCII 2   /* -a      */
#define M_MAG     3   /* -m      */
#define M_DOTS    4   /* -d      */

const char *cmd_tokens[256];
const char  *op_tokens[256];

int bas_type = B_ATARI; /* -b */

int output_mode = M_UTF8;

int bold =        0;  /* 1 with -B */
int color =       1;  /* 0 with -n */
int immediate =   0;  /* 1 with -i */
int underline =   0;  /* 1 with -u */
int skip_lineno = 0;  /* 1 with -l */
int dump_tables = 0;  /* 1 with -D */
int autodetect  = 1;  /* 0 with -b */
int mixed_case  = 1;  /* 0 with -k, or if input is B_ATARI or B_APLUS */
int indent =      1;  /* 0 with -t, or if input is B_ATARI */
int startline =   0;  /* -r */
int endline   =   32767;  /* -r */

/* change these with -c */
int color_cmd =       C_YELLOW;
int color_op  =       C_GREEN;
int color_func =      C_PURPLE;
int color_const =     C_RED;
int color_lineno =    C_CYAN;
int color_rem =       C_BLUE;
int color_data =      C_CYAN;
int color_varnames  = NO_COLOR;

const char *input_filename;

int indent_level =    0;
int if_without_then = 0;
int badtok =          0; /* set to 1 if we find a bad token */
int inv =             0; /* set to 1 when we're printing inverse */
int cur_color =      -1; /* -1 = no color */

int first_stmt = 1; /* true if cmd token comes right after lineno */

FILE *outfh;

int parse_color(char c) {
	if(c == 'n')
		return NO_COLOR;

	if(c >= '0' && c <= '7')
		return c - '0';

	fprintf(stderr, "%s: Invalid color '%c'.\n", self, c);
	exit(1);
}

void parse_color_scheme(const char *arg) {
	if(!arg) return;

	if(strlen(arg) != 8)
		die("Color scheme must be 8 characters.");

	color_cmd =       parse_color(arg[0]);
	color_op  =       parse_color(arg[1]);
	color_func =      parse_color(arg[2]);
	color_const =     parse_color(arg[3]);
	color_lineno =    parse_color(arg[4]);
	color_rem =       parse_color(arg[5]);
	color_data =      parse_color(arg[6]);
	color_varnames  = parse_color(arg[7]);
}

int get_bas_type(char *arg) {
	if(arg[0] == 't')
		return B_TURBO;

	if(arg[0] == 'a') {
		if(arg[1] == '+')
			return B_APLUS;
		else
			return B_ATARI;
	}

	if(arg[0] == 'x') {
		if(arg[1] == 'l')
			return B_BXL;
		else if(arg[1] == 'e')
			return B_BXE;
	}

	fprintf(stderr, "%s: Invalid BASIC type for -b option: %s\n", self, arg);
	exit(1);
}

void get_line_range(const char *arg) {
	int val = 0, comma = 0;
	const char *p = arg;

	while(*p) {
		if(*p >= '0' && *p <= '9') {
			val *= 10;
			val += *p - '0';
			if(val > 32768) die("Invalid line number for -r (range is 0-32768).");
		} else if(*p == ',' || *p == '-') {
			if(comma) die("Invalid argument for -r (too many commas).");
			comma++;
			startline = val;
			val = 0;
		} else {
			if(comma) die("Invalid argument for -r (only digits and comma allowed).");
		}
		p++;
	}

	if(comma)
		endline = val ? val : 32767;
	else
		startline = endline = val;

	if(endline < startline)
		die("Invalid argument for -r (start > end).");
}

void print_help(void) {
	printf("Usage: %s [-a|-d|-m|-x|-U] [-B] [-i] [-l] [-u] [-n|-C] [-v] [-c *colors*] [-r *start,end* ] [-k] [-t] <inputfile>\n", self);
	printf("  -b <XX>: set BASIC type. XX is: a = atari, t = turbo, xl, xe, a+.\n");
	printf("  -U: output ATASCII as Unicode/UTF-8 (this is the default).\n");
	printf("  -a: output raw ATASCII.\n");
	printf("  -d: use dots instead of Unicode/UTF-8.\n");
	printf("  -m: magazine style listing (see a8cat(1)).\n");
	printf("  -x: XL international character set (UTF-8).\n");
	printf("  -B: use bold for color output.\n");
	printf("  -i: show immediate mode command (line 32768).\n");
	printf("  -l: don't print line numbers.\n");
	printf("  -C: enable color syntax highlighting (this is the default).\n");
	printf("  -n: disable color syntax highlighting.\n");
	printf("  -u: use underline for inverse video.\n");
	printf("  -c: use custom colors (see man page).\n");
	printf("  -k: disable mixed case keywords for BXL/BXE (e.g. Print).\n");
	printf("  -t: disable Turbo/BXL/BXE indentation.\n");
	printf("  -r: only list lines numbered from *start* to *end*.\n");
	printf("  -v: verbose.\n");
}

void parse_args(int argc, char **argv, int from_env) {
	int opt;

	optind = 1;

	while( (opt = getopt(argc, argv, "r:Db:UCviamnBdhxulc:kt")) != -1) {
		switch(opt) {
			case 'U': output_mode = M_UTF8;    break;
			case 'a': output_mode = M_ATASCII; break;
			case 'm': output_mode = M_MAG;     break;
			case 'd': output_mode = M_DOTS;    break;
			case 'x': output_mode = M_UTF8_I;  break;
			case 'D': dump_tables = 1; break;
			case 'v': verbose =     1; break;
			case 'i': immediate =   1; break;
			case 'B': bold =        1; break;
			case 'u': underline =   1; break;
			case 'C': color =       1; break;
			case 'n': color =       0; break;
			case 'l': skip_lineno = 1; break;
			case 'k': mixed_case =  0; break;
			case 't': indent =      0; break;
			case 'b': autodetect = 0; bas_type = get_bas_type(optarg); break;
			case 'r': get_line_range(optarg); break;
			case 'c': parse_color_scheme(optarg); break;
			case 'h': print_help(); exit(0);
			default:
				print_help();
				exit(1);
		}
	}

	if(output_mode == M_ATASCII) color = 0;

	if(!from_env) {
		if(optind >= argc) {
			die("No input file given (use - for stdin).");
		} else {
			input_filename = argv[optind];
			open_input(input_filename);
		}
	}
}

/* make a fake argv and argc */
void parse_env_args(void) {
	int fargc;
	char *fargv[ENV_MAX_ARGS + 1];
	char *env, *p;

	env = getenv(ENV_OPTS);
	if(!env) return;

#ifdef DEBUG_ENV
	fprintf(stderr, "%s: " ENV_OPTS " is set in the env, using it.\n", self);
#endif

	fargv[0] = (char *)self;
	fargv[1] = env;
	fargc = 2;

	for(p = env; *p; p++) {
		if(*p == ' ' || *p == '\t') {
			*p = '\0';
			if(fargc == ENV_MAX_ARGS) {
				/* not gonna bother with the man page for this, it's
				   long enough already. it's a pathological case. */
				fprintf(stderr, "%s: Too many spaces in " ENV_OPTS " (max %d)\n", self, ENV_MAX_ARGS - 1);
				break;
			}
			fargv[fargc++] = p + 1;
		}
	}

	fargv[fargc] = NULL;

#ifdef DEBUG_ENV
	{
		char **p;
		fprintf(stderr, "%s: Read options from environment:\n", self);
		for(p = fargv + 1; *p; p++) {
			fprintf(stderr, "\t%s\n", *p);
		}
	}
#endif

	parse_args(fargc, fargv, 1);
}

void setup_outfh(void) {
	if(output_mode == M_ATASCII) {
		if(isatty(fileno(stdout))) {
			die("Refusing to write ATASCII data to the terminal.");
		}
	}
	outfh = freopen(NULL, "wb", stdout);
	if(!outfh) {
		perror("freopen()");
		die("Can't reopen standard output in binary mode.");
	}
	return;
}

void outchr(char c) {
	putc(c, outfh);
}

/* this should probably be moved to bcdfp.c */
double bcd2double(const unsigned char *num) {
	double result = 0, sign;
	int exp, i;

	exp = *num;
	if(!exp) {
		return 0.0;
	}

	sign = (exp & 0x80 ? -1.0 : 1.0);
	exp &= 0x7f;
	exp -= 0x40;

	for(i = 1; i < 6; i++) {
		result *= 100.0;
		result += bcd2int(num[i]);
	}

	result *= pow(100, exp - 4);
	result *= sign;

	return result;
}

void color_on(int c) {
	cur_color = c;
	if(c != NO_COLOR)
		printf(COLOR_FMT, bold, c);
}

void color_off(void) {
	cur_color = -1;
	fputs(COLOR_OFF, outfh);
}

void print_number(unsigned int pos, int hex) {
	double num = bcd2double(program + pos);
	if(color) color_on(color_const);
	if(hex) {
		if(num < 0 || num > 0xffff) {
			fprintf(stderr, "%s: invalid Turbo/BXL/BXE hex const %lf at pos $%04x\n", self, num, pos);
		}
		fprintf(outfh, (num > 0xff ? "%04x" : "%02x"), (unsigned int)num);
	} else {
		fprintf(outfh, "%G", num);
	}
	if(color) color_off();
}

/* only called in magazine mode. cursor control characters
   like a bell in the middle of a non-inverse string
   should not cause it to print {inv}{bell}{norm}. The {bell}
   is "inherently" inverse (since its high bit is set) but doesn't
   need to be printed that way. */
int affects_inv(unsigned char c) {
	switch(c) {
		case 0x1b: /* esc */
		case 0x1c: /* up */
		case 0x1d: /* down */
		case 0x1e: /* left */
		case 0x1f: /* right */
		case 0x9b: /* EOL */
		case 0x9c: /* del line */
		case 0x9d: /* ins line */
		case 0x9e: /* clear tab */
		case 0x9f: /* set tab */
		case 0x7d: /* cls */
		case 0x7e: /* BS */
		case 0x7f: /* tab */
		case 0xfd: /* bell */
		case 0xfe: /* del chr */
		case 0xff: /* ins chr */
			return 0;
		default:
			return 1;
	}
}

void start_inv(unsigned char c) {
	switch(output_mode) {
		case M_DOTS:
		case M_UTF8:
		case M_UTF8_I:
			if(underline)
				fputs(ULINE_ON, outfh);
			else
				fputs(INV_ON, outfh);
			inv = 1;
			break;
		case M_MAG:
			if(affects_inv(c)) {
				fputs(MAG_INV, outfh);
				inv = 1;
			}
			break;
		case M_ATASCII:
		default:
			break;
	}
}

void end_inv(unsigned char c) {
	switch(output_mode) {
		case M_DOTS:
		case M_UTF8:
		case M_UTF8_I:
			fputs(INV_OFF, outfh);
			inv = 0;
			/* work around INV_OFF also turning off color. */
			if(cur_color > -1)
				color_on(cur_color);
			break;
		case M_MAG:
			if(affects_inv(c)) {
				fputs(MAG_NORM, outfh);
				inv = 0;
			}
			break;
		case M_ATASCII:
		default:
			break;
	}
}

void print_ata_chr(unsigned char c) {
	if(c & 0x80) {
		if(!inv) {
			start_inv(c);
		}
	} else {
		if(inv) {
			end_inv(c);
		}
	}

	switch(output_mode) {
		case M_DOTS:
			fputc(isprint(c & 0x7f) ? c & 0x7f : '.', outfh);
			break;
		case M_UTF8:
			fputs(ata2utf[c & 0x7f], outfh);
			break;
		case M_UTF8_I:
			fputs(ics2utf[c & 0x7f], outfh);
			break;
		case M_MAG:
			fputs(ata2mag[c & 0x7f], outfh);
			break;
		case M_ATASCII:
		default:
			outchr(c);
			break;
	}
}

void print_mixed_case(const char *name) {
	char c;

	if(!*name) return;
	if(*name == ' ')
		outchr(*name++);
	outchr(*name++);
	while(( c = *name++)) {
		if(c >= 'A' && c <= 'Z')
			c |= 0x20;
		if(color && c == '(' && cur_color != color_op) color_on(color_op);
		outchr(c);
	}
}

void print_string(unsigned int pos, unsigned int len) {
	inv = 0;
	if(color) color_on(color_op);
	outchr('"');
	if(color) color_on(color_const);
	while(len--) print_ata_chr(program[pos++]);
	if(inv) {
		end_inv(0);
	}
	if(color) color_on(color_op);
	outchr('"');
	if(color) color_off();
}

const char *get_bxl_ext_name(unsigned char tok) {
	switch(tok) {
		case 0x10:
			return "Local";
		case 0x11:
			return "Exit";
		case 0x12:
			return "Procedure";
		case 0x13:
			return "Call";
		case 0x14:
			return "Sortup";
		case 0x15:
			return "Sortdown";
		default:
			return "(bad BXL ext token)";
	}
}

void clear_callbacks(void);
void init_callbacks(void);

int cmd_is_if;

CALLBACK(find_cmd_if) {
	cmd_is_if = 0;
	if((bas_type == B_APLUS && tok == 0x06) || (bas_type != B_APLUS && tok == CMD_IF)) {
		cmd_is_if = 1;
		if_without_then++; /* find_then() decrements it, if there's a THEN */
	}
}

CALLBACK(find_op_then) {
	if(verbose)
		fprintf(stderr, "find_op_then lineno %d, pos %04x, tok %02x\n", lineno, pos, tok);

	if(cmd_is_if && tok == OP_THEN)
		if_without_then--;
}

/* walk_code()'s API isn't really set up to be reentrant, but we
   can do one level of sub-walk. */
void find_then(int lineno, unsigned short pos) {
	clear_callbacks();
	on_exp_token = find_op_then;
	on_cmd_token = find_cmd_if;
	walk_code(lineno, lineno);
	init_callbacks();
}

CALLBACK(print_lineno) {
	unsigned char cmd_tok;

	first_stmt = 1;

	if(indent) {
		cmd_tok = program[pos + 4];
		if_without_then = 0;
		if((bas_type == B_APLUS && cmd_tok == 0x06) || (bas_type != B_APLUS && cmd_tok == CMD_IF)) {
			find_then(lineno, pos);
		}
	}

	if(skip_lineno) return;
	if(color) color_on(color_lineno);
	fprintf(outfh, "%d ", lineno);
	if(color) color_off();
}

/* indentation is totally different between A+, BXL, and BXE.
   Turbo and BXE use the same indent style, but Turbo has more
   keywords that indent (e.g. REPEAT, DO).

   A+ always indents on the lines after a WHILE or a FOR, unless there's
   a matching ENDWHILE or NEXT on the same line. It indents the lines
   after IF only if there's no THEN and no matching ENDIF on the
   same line. ENDWHILE, NEXT, ENDIF all un-undent one level. The style
   is that the the ending keyword (or ELSE) does NOT get unindented,
   itself:

   WHILE 1
     PRINT "OK"
     ENDWHILE
   PRINT "HEY"

   That looks *wrong* to me (the ENDWHILE should line up with the WHILE), but
   that's how A+ works.

  BXL's style is to indent the entire block to the same level:

     While 1
     Print "OK"
     Endwhile
   Print "HEY"

   This applies even if the whole block's on one line:

    ? "OK"
      For I=1 To 10:Next I
    ? "OK"

BXE and Turbo work like modern style: the keywords that start and end the
block are not indented, only the lines between them.

   While 1
     Print "OK"
   Endwhile
   Print "HEY"

 */

void print_indent(void) {
	int i;
	if(indent_level < 0) indent_level = 0;
	for(i = 0; i < indent_level; i++) {
		outchr(' ');
		outchr(' ');
	}
}

void aplus_indent_line(const unsigned char tok) {
	if(first_stmt) print_indent();

	switch(tok) {
		case 0x07: /* FOR */
		case 0x13: /* WHILE */
			indent_level++;
			return;
		case 0x08: /* NEXT */
		case 0x14: /* ENDWHILE */
		case 0x18: /* ENDIF */
			indent_level--;
			return;
		default: break;
	}
	if(if_without_then) {
		indent_level++;
		if_without_then = 0;
	}
}

void turbo_indent_line(const unsigned char tok) {
	switch(tok) {
		case 0x40:    /* ELSE */
			if(first_stmt) {
				/* gets "outdented" */
				indent_level--;
				print_indent();
				indent_level++;
			}
			return;
		case CMD_FOR: /* FOR */
		case 0x3c:    /* REPEAT */
		case 0x3e:    /* WHILE */
		case 0x45:    /* DO */
		case 0x4f:    /* PROC */
			if(first_stmt) print_indent();
			indent_level++;
			return;
		case CMD_NEXT: /* NEXT */
		case 0x3d:     /* UNTIL */
		case 0x3f:     /* WEND */
		case 0x41:     /* ENDIF */
		case 0x46:     /* LOOP */
		case 0x51:     /* ENDPROC */
			indent_level--;
			break;
		default: break;
	}
	if(first_stmt) print_indent();
	if(if_without_then) {
		indent_level++;
		if_without_then = 0;
	}
}

/* Note: BXL's PROCEDURE/EXIT (from toolkit extensions) does
   *not* get indented. */
void bxl_indent_line(unsigned char tok) {
	if(if_without_then) {
		indent_level++;
		if_without_then = 0;
	}
	switch(tok) {
		case CMD_FOR: /* FOR */
		case 0x38:    /* WHILE */
			indent_level++;
			break;
		case CMD_NEXT: /* NEXT */
		case 0x39:     /* ENDWHILE */
		case 0x3d:     /* ENDIF */
			if(first_stmt) print_indent();
			indent_level--;
			return;
		default: break;
	}
	if(first_stmt) print_indent();
}

void bxe_indent_line(const unsigned char tok) {
	switch(tok) {
		case 0x3c:    /* ELSE */
			if(first_stmt) {
				/* gets "outdented" */
				indent_level--;
				print_indent();
				indent_level++;
			}
			return;
		case CMD_FOR: /* FOR */
		case 0x38:    /* WHILE */
		case 0x59:    /* PROCEDURE */
			if(first_stmt) print_indent();
			indent_level++;
			return;
		case CMD_NEXT: /* NEXT */
		case 0x39:     /* ENDWHILE */
		case 0x5e:     /* EXIT */
		case 0x3d:     /* ENDIF */
			indent_level--;
			break;
		default: break;
	}
	if(first_stmt) print_indent();
	if(if_without_then) {
		if_without_then = 0;
		indent_level++;
	}
}

void indent_line(const unsigned char tok) {
	if(!indent) return;

	switch(bas_type) {
		case B_APLUS: aplus_indent_line(tok); return;
		case B_TURBO: turbo_indent_line(tok); return;
		case   B_BXL:   bxl_indent_line(tok); return;
		case   B_BXE:   bxe_indent_line(tok); return;
		case B_ATARI:
		default:
			return;
	}
}

CALLBACK(print_cmd) {
	const char *name;

	indent_line(tok);
	if(first_stmt)
		first_stmt = 0;

	if(bas_type == B_APLUS) {
		if(tok == 0x52) return;
	} else {
		if(tok == CMD_ILET) return;
	}

	if(color) color_on(color_cmd);

	if((!(name = cmd_tokens[tok]))) {
		fprintf(outfh, "(bad cmd token $%02x) ", tok);
		badtok = 1;
	} else {
		if(bas_type == B_BXL && tok == 0x5a)
			name = get_bxl_ext_name(program[pos + 1]);

		if(mixed_case && (bas_type == B_BXL || bas_type == B_BXE)) {
			print_mixed_case(name);
			outchr(' ');
		} else {
			fprintf(outfh, "%s ", name);
		}
	}

	if(color) color_off();
}

void aplus_op_color_on(unsigned char tok) {
	int c = color_op;

	if((tok >= 0x17 && tok <= 0x1c) || (tok >= 0x29 && tok <= 0x2b)) {
		/* keywords */
		c = color_cmd;
	} else if(tok >= 0x40) {
		/* functions */
		c = color_func;
	} else if(tok == 0x38 || tok == 0x39) {
		/* unary +/- */
		c = color_const;
	}

	color_on(c);
}

/* pseudo-funcs listed here are ones that don't take (), e.g.
   X=RND or A$=INKEY$. right now they're colored the same as regular
   functions, but it might make sense to color them as variables?
   also, %0 %1 %2 %3 (tokens 0x66 to 0x69) are currently op-colored,
   perhaps they should be constant-colored? */
void turbo_op_color_on(unsigned char tok) {
	switch(tok) {
		case 0x55:
		case 0x58:
		case 0x59: /* pseudo-func */
		case 0x5b:
		case 0x5c:
		case 0x5e:
		case 0x5f: /* pseudo-func */
		case 0x60: /* pseudo-func */
		case 0x63: /* pseudo-func */
		case 0x64:
		case 0x65:
		case 0x6b:
		case 0x6c: /* pseudo-func */
		case 0x6d: /* pseudo-func */
			color_on(color_func);
			break;
		case 0x5a:
		case 0x5d:
		case 0x61:
		case 0x62:
		case 0x6a:
			color_on(color_cmd);
			break;
		default:
			color_on(color_op);
			break;
	}
}

/* BXL and BXE use the same operator tokens */
void bxlxe_op_color_on(unsigned char tok) {
	int c = color_op;

	if(tok == 0x55) {
		c = color_cmd;
	} else if(tok >= 0x5a) {
		c = color_func;
	}

	color_on(c);
}

void op_color_on(unsigned char tok) {
	if(!color) return;

	if(bas_type == B_APLUS) {
		aplus_op_color_on(tok);
		return;
	}

	/* common ops for Atari, Turbo, BXL, BXE */
	if(tok <= last_operator) {
		if(tok > 0x3c)
			color_on(color_func);
		else if(tok == OP_UMINUS || tok == OP_UPLUS)
			color_on(color_const); /* show leading sign in same color as the number */
		else if((tok >= 0x17 && tok <= 0x1b) || (tok >= 0x28 && tok <= 0x2a))
			color_on(color_cmd);
		else
			color_on(color_op);
		return;
	}

	if(bas_type == B_TURBO)
		turbo_op_color_on(tok);
	else if(bas_type == B_BXL || bas_type == B_BXE)
		bxlxe_op_color_on(tok);
	else
		color_on(color_op); /* in case of bad token when bas_type == B_BASIC */
}

void print_varname(unsigned char varnum) {
	int i, count;
	unsigned char c;

	if(color) color_on(color_varnames);

	for(i = vnstart, count = 0; count < varnum; i++) {
		if(program[i] & 0x80) count++;
		if(i == codestart) {
			fprintf(outfh, "(bad var tok $%02x)", varnum | 0x80);
			return;
		}
	}

	do {
		c = program[i++];
		if(color && c == ('(' | 0x80)) {
			if(color) color_on(color_op);
		}
		outchr(c & 0x7f);
	} while (c < 0x80);
	if(color) color_off();
}

CALLBACK(print_op) {
	const char *name;

	switch(tok) {
		case 0: /* Turbo variables numbered >= $80 */
			if(bas_type == B_TURBO) {
				print_varname(program[pos + 1] | 0x80);
				return;
			} else {
				fprintf(stderr, "%s: got Turbo ext var tok at line %d, in non-Turbo program!\n", self, lineno);
			}
			break;
		case OP_HEXCONST:
			if(color) color_on(color_op);
			outchr('$');
			print_number(pos + 1, 1);
			return;
		case OP_NUMCONST:
			print_number(pos + 1, 0);
			return;
		case OP_STRCONST:
			print_string(pos + 2, program[pos + 1]);
			return;
		case OP_EOL:
			return;
		default: break;
	}

	if(color) op_color_on(tok);

	if((!(name = op_tokens[tok]))) {
		fprintf(outfh, "(bad op token $%02x)", tok);
		badtok = 1;
	} else {
		if(bas_type == B_BXL || bas_type == B_BXE)
			print_mixed_case(name);
		else
			fprintf(outfh, "%s", name);
	}
	if(color) color_off();
}

/* for normal BASIC/A+/XL/XE variable tokens, $80-$FF. Turbo uses
   these, too, but it supports 256 variables, so it only uses $80-$FF
   for the first 128 vars. */
CALLBACK(print_std_varname) {
	print_varname(tok & 0x7f);
}

CALLBACK(print_text) {
	unsigned char c, is_data = program[pos - 1] == CMD_DATA, comma = 0;

	inv = 0;
	if(color) color_on(is_data ? color_data : color_rem);
	while(program[pos] != 0x9b) {
		c = program[pos++];
		if(color && is_data && c == ',') {
			color_on(color_op);
			comma = 1;
		} else {
			comma = 0;
		}
		print_ata_chr(c);
		if(comma)
			color_on(is_data ? color_data : color_rem);
	}
	if(inv) end_inv(0);
	if(color) color_off();
}

CALLBACK(print_newline) {
	if(output_mode == M_ATASCII)
		outchr(0x9b);
	else
		outchr('\n');
}

CALLBACK(code_prot) {
	fprintf(stderr, "%s: Program is code-protected, stopping at line %d.\n", self, lineno);
	exit(0);
}

void clear_callbacks(void) {
	on_start_line = 0;
	on_cmd_token = 0;
	on_exp_token = 0;
	on_var_token = 0;
	on_end_line = 0;
	on_text = 0;
	on_bad_line_length = 0;
}

void init_callbacks(void) {
	on_start_line = print_lineno;
	on_cmd_token = print_cmd;
	on_exp_token = print_op;
	on_var_token = print_std_varname; /* see also: print_turbo_varname() */
	on_end_line = print_newline;
	on_text = print_text;
	on_bad_line_length = code_prot;
}

void list(void) {
	init_callbacks();
	if(endline == 32767 && immediate) endline++;
	walk_code(startline, endline);
}

void init_bas_tables() {
	memmove(cmd_tokens, commands, (last_command + 1) * sizeof(char *));
	memmove(op_tokens, operators, (last_operator + 1) * sizeof(char *));
}

void init_aplus_tables() {
	memmove(cmd_tokens, aplus_cmds, aplus_cmd_size);
	memmove(op_tokens, aplus_ops, aplus_ops_size);
}

void init_turbo_tables() {
	memmove(cmd_tokens + last_command  + 1, turbo_cmds, turbo_cmd_size);
	memmove(op_tokens  + last_operator + 1, turbo_ops,  turbo_ops_size);
}

void init_bxl_tables() {
	memmove(cmd_tokens + last_command  + 1, bxl_cmds, bxl_cmd_size);
	memmove(op_tokens  + last_operator + 1, bxl_ops,  bxl_ops_size);
}

/* BXE's token table is identical to BXL's, and the commands are the
   same up to token 0x56 (FAST). */
void init_bxe_tables() {
	init_bxl_tables();
	memmove(cmd_tokens + 0x57, bxe_cmds, bxe_cmd_size);
}

void init_token_tables() {
	if(bas_type == B_APLUS) {
		init_aplus_tables();
		return;
	}

	init_bas_tables();

	if(bas_type == B_TURBO)
		init_turbo_tables();
	else if(bas_type == B_BXL)
		init_bxl_tables();
	else if(bas_type == B_BXE)
		init_bxe_tables();

	if(dump_tables) {
		int i;
		printf("commands:\n\n");
		for(i = 0; i < 0x65; i++)
			printf("%s $%02x\n", (cmd_tokens[i] ? cmd_tokens[i] : "(null)"), i);
		printf("operators:\n\n");
		for(i = 0x12; i < 0x69; i++)
			printf("%s $%02x\n", (op_tokens[i] ? op_tokens[i] : "(null)"), i);
		exit(0);
	}
}

void set_bas_dialect(int d) {
	if(verbose)
		fprintf(stderr, "set_bas_dialect(%d)\n", d);

	switch(d) {
		case 0:
		case 1:
			die("whichbas child process failed");
			break;
		case SRET_ATARI:
		case SRET_APLUS:
		case SRET_TURBO:
		case SRET_BXL:
		case SRET_BXE:
			bas_type = d;
			break;
		default:
			fprintf(stderr, "whichbas results ambiguous; guessing Turbo BASIC\n");
			bas_type = SRET_TURBO;
			break;
	}
}

void detect_bas_dialect() {
	pid_t pid, status;
	int wstatus;
	const char *args[4];

	args[0] = "whichbas";
	args[1] = "-s";
	args[2] = input_filename;
	args[3] = 0;

	pid = fork();
	if(pid == -1) {
		perror("fork()");
		die("Can't spawn child process");
	} else if(pid) {
		/* we are the parent */
		status = waitpid(pid, &wstatus, 0);
		if(status < 0) {
			perror("waitpid()");
			die("Child process went south");
		}
		if(!WIFEXITED(wstatus)) {
			die("Child process went south");
		}
		set_bas_dialect(WEXITSTATUS(wstatus));
	} else {
		/* we are the child */
		if(execvp(args[0], (char * const *)args) < 0) {
			perror("Can't execute whichbas");
			exit(1);
		}
	}
}

void init_bas_dialect() {
	if(autodetect)
		detect_bas_dialect();

	if(bas_type == B_TURBO || bas_type == B_BXL || bas_type == B_BXE)
		allow_hex_const = 1;

	if(bas_type == B_APLUS)
		aplus_errtok_hack = 1;

	if(bas_type == B_BXL)
		bxl_exttok_hack = 1;

	if(bas_type == B_ATARI || bas_type == B_APLUS || bas_type == B_TURBO)
		mixed_case = 0;

	if(bas_type == B_ATARI)
		indent = 0;
}

int main(int argc, char **argv) {
	set_self(*argv);
	parse_general_args(argc, argv, print_help);
	parse_env_args();
	parse_args(argc, argv, 0);

	init_bas_dialect();
	init_token_tables();

	readfile();
	parse_header();

	if(!vntable_ok())
		die("Program is variable-protected; unprotect it first.");

	setup_outfh();
	list();

	if(badtok) {
		fprintf(stderr, "%s: Program has unknown tokens; wrong BASIC dialect?\n", self);
		return 2;
	}

	return 0;
}
