#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdarg.h>
#include <ctype.h>

#include "amsbtok.h"

/* this should always be defined in <stdio.h>, but just in case... */
#ifndef BUFSIZ
#define BUFSIZ 4096
#endif

/* range for one-byte tokens */
#define MIN_STD_TOK 0x80 /* END */
#define MAX_STD_TOK 0xf8 /* <   */

/* range for 2nd byte of two-byte tokens (1st is always 0xff) */
#define MIN_EXT_TOK 0xa3 /* SGN */
#define MAX_EXT_TOK 0xc5 /* STACK */

/* AMSB's tokens for "!", "'", REM. used to introduce comments */
#define TOK_REM    0x98
#define TOK_SQUOTE 0x9a
#define TOK_BANG   0x9b

/* various other AMSB tokens */
#define TOK_ELSE   0x9c
#define TOK_IF     0x97
#define TOK_THEN   0x9d
#define TOK_AND    0xf3
#define TOK_OR     0xf4
#define TOK_NOT    0xf5

/* good old Atari EOL character */
#define EOL 0x9b

/* every MS BASIC I ever saw had the same line number limit. it's
   kind of arbitrary: why not allow the full range, up to 65535? */
#define MAX_LINENO 63999

/* a program bigger than this can't possibly fit into memory,
   even with cart-based AMSB2 and no DOS loaded. */
#define MAX_PROGLEN 30000

/* there should never be a valid line of BASIC longer than MAX_LINE_LEN
   bytes, since there would be no way to enter it in the editor. AMSB
   uses the standard E: device, which limits you to 3 40-column screen
   lines, or max 120 bytes (after POKE 82,0). there *cannot* be a line
   longer than MAX_LINE_LEN_HARD, because AMSB would crash when you
   try to LOAD such a program. */
#define MAX_LINE_LEN 0x80
#define MAX_LINE_LEN_HARD 0xff

/* a program whose header has a length less than MIN_PROGLEN can't be
   a real AMSB program. EMPTY_PROGLEN is what you get if you
   SAVE when there's no program in memory (right after boot or
   a NEW). The minimum size for a program that actually contains
   code seems to be 5 (for 10 PRINT) */
#define MIN_PROGLEN 5
#define EMPTY_PROGLEN 2

/* a file shorter than this can't be an AMSB program */
#define MIN_BYTES 5

/* an EOL address below this has to be an error, since
   this is the lowest MEMLO can ever be. */
#define MIN_PTR 0x0700

/* an EOL address higher than this has to be an error, since
	it would overlap the GR.0 display list or the ROMs at $c000 */
#define MAX_PTR 0xbc1f

/* SAVE "filename" LOCK does 'encryption' by subtracting every byte
   from this (except the 3-byte header) */
#define UNLOCK_KEY 0x54

const char *self;

char pipe_command[BUFSIZ + 1] = { "a8cat" };

int verbosity =   0;           /* -v */
int raw_output =  0;           /* -a */
int check_only =  0;           /* -c */
int startline =   0;           /* -r */
int endline   =   65536;       /* -r */
int unlock_mode = 0;           /* -l */
int initial_eol = 1;           /* -n */
int crunch      = 0;           /* -C, -D */
int decrunch    = 0;           /* -D */
int keep_rems   = 0;           /* -k */

int locked =      0;
int need_pclose = 0;
int bytes_read =  0;
int warnings =    0;
int proglen =     0;
int linecount =   0;
int header_read = 0;
int printing    = 0;
int spacecount  = 0;

FILE *infile;
FILE *outfile;

const char *outname = NULL;

void verbose(int level, const char *fmt, ...) {
	va_list ap;

	if(verbosity < level) return;

	va_start(ap, fmt);
	vfprintf(stderr, fmt, ap);
	va_end(ap);
	fputc('\n', stderr);
}

void finish(int rv);

#define VA_FUNC(func, prefix, action) \
	void func(const char *fmt, ...) { \
		va_list ap; \
		va_start(ap, fmt); \
		fprintf(stderr, "%s: %s: ", self, prefix); \
		vfprintf(stderr, fmt, ap); \
		va_end(ap); \
		fputc('\n', stderr); \
		action; \
	}

VA_FUNC(die,    "error",   finish(2));
VA_FUNC(os_err, "fatal",   exit(1));
VA_FUNC(warn,   "warning", warnings++);

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

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

const char *plural(int count) {
	return count == 1 ? "" : "s";
}

int is_comment_start(unsigned char tok) {
	return (tok == TOK_REM || tok == TOK_SQUOTE || tok == TOK_BANG);
}

/* post-processing: print "summary", exit. called by either die()
	or main() (on normal exit). */
void finish(int rv) {
	int progsize;

	verbose(1, "read %d bytes", bytes_read);
	verbose(1, "listed %d line%s", linecount, plural(linecount));

	if(!linecount) warn("no lines of code in program");

	if(header_read) {
		progsize = bytes_read - 3;
		if(proglen == progsize) {
			verbose(1, "file size matches proglen");
		} else {
			warn("program size %d doesn't match size %d in header,",
					progsize, proglen);
			fputs("          ", stderr);
			if(proglen > progsize) {
				fputs("AMSB will give #136 ERROR and fail to LOAD this file\n", stderr);
			} else {
				fputs("AMSB will stop LOADing before the end of this file\n", stderr);
			}
		}
	} else {
		warn("file is %d byte%s, too short to be an AMSB program", bytes_read, plural(bytes_read));
	}

	if(fgetc(infile) != EOF) warn("trailing garbage at end of file");

	if(warnings) {
		fprintf(stderr, "%s: file has %d warning%s\n",
			self, warnings, plural(warnings));
		rv = 2;
	}

	if(need_pclose) {
		int got = pclose(outfile);
		verbose(1, "return value from pipe is %d", got);
		if(got != 0) {
			os_err("a8cat child process failed, do you have a8cat on your PATH?");
		}
	}

	verbose(2, "spaces outside strings/comments: %d (%.1f%%)",
		spacecount,
		(float)(spacecount) / (float)(bytes_read) * 100.0);

	verbose(1, "exit status: %d (%s)", rv, (rv ? "ERROR" : "OK"));

	exit(rv);
}

unsigned char read_byte(void) {
	int c;

	c = fgetc(infile);

	if(c < 0) die("unexpected EOF in %s (byte %d), file truncated?",
			(header_read ? "program" : "header"), bytes_read);

	bytes_read++;
	return (unsigned char)c;
}

/* "decrypt" a byte from a "SAVE x LOCK" program. */
unsigned char unlock_byte(unsigned char b) {
	return ((UNLOCK_KEY - b) & 0xff);
}

/* the "encryption" is the same (process is reciprocal) */
#define lock_byte(x) unlock_byte(x)

/* read and (if needed) decrypt a byte from the program. */
unsigned char read_prog_byte(void) {
	unsigned char b = read_byte();
	return locked ? unlock_byte(b) : b;
}

/* read a word from the program, does not decrypt */
int read_word(void) {
	int w;

	w = read_byte();
	w |= (read_byte() << 8);

	return w;
}

/* read and (if needed) decrypt a word from the program. */
int read_prog_word(void) {
	int w;

	w = read_prog_byte();
	w |= (read_prog_byte() << 8);

	return w;
}

void read_header(void) {
	/* $00 for plain, $01 for SAVE with LOCK */
	locked = read_byte();
	if(locked > 1) die("not an AMSB file: first byte is $%02x, not $00 or $01", locked);

	if(locked) verbose(1, "program is locked, decrypting");

	proglen = read_word();

	verbose(1, "proglen == %d (%04x)", proglen, proglen);

	if(proglen > MAX_PROGLEN) {
		die("not an AMSB file: too big (%d bytes), won't fit in Atari memory", proglen);
	}

	if(proglen == EMPTY_PROGLEN) {
		warn("program length is 2, no code in file (SAVE after NEW)");
	} else {
		if(proglen < MIN_PROGLEN) {
			die("not an AMSB file: program size too small (%d). Atari BASIC file?", proglen);
		}
	}

	header_read = 1;
}

void unknown_token(int tok) {
	int ext;
	if(!printing) return;
	ext = (tok > 0xff);
	tok &= 0xff;
	fprintf(outfile, "<unknown %stoken ", (ext ? "extended " : ""));
	fprintf(outfile, "%s%02x>", (ext ? "$ff ": ""), tok);
}

void list_char(unsigned char c) {
	if(printing) fputc(c, outfile);
}

int expand_token(int t, unsigned char *buf);

int list_token(int tok) {
	unsigned char text[10];
	if(!expand_token(tok, text)) {
		unknown_token(tok);
		return 0;
	}
	if(printing) fputs((char *)text, outfile);
	return 1;
}

void list_lineno(int l) {
	/* note that AMSB always puts a space after the line number in LIST */
	if(printing) fprintf(outfile, "%d ", l);
}

void start_listing(void) {
	/* AMSB always prints a blank line when it LISTs, so we do, too.
	   unless it's disabled with -n, of course. */
	if(initial_eol) fputc(EOL, outfile);
}

int read_token(int literal);

/* meat and potatoes. does the actual detokenizing. gets called once
   per line of code. returns false when it hits the last line, or
   true if there are more lines. */
int next_line(void) {
	static int last_lineno = -1;
	static int last_ptr = -1;
	int tok, ptr, lineno, was_colon = 0, in_string = 0, in_comment = 0, offset, len;

	offset = bytes_read;

	/* pointer to last token on the line, offset by whatever MEMLO
		happened to be when the file was SAVEd. 0 means this is the
		last line. */
	ptr = read_prog_word();
	if(!ptr) {
		verbose(1, "end of program");
		return 0;
	}

	lineno = read_prog_word();
	verbose(2, "found line %d, offset %d, end-of-line %d", lineno, offset, ptr);

	printing = (lineno >= startline) && (lineno <= endline);

	if(ptr < MIN_PTR) {
		warn("line %d: EOL address $%04x too low (<$%04x)", lineno, ptr, MIN_PTR);
	} else if(ptr >= MAX_PTR) {
		warn("line %d: EOL address $%04x too high (>$%04x)", lineno, ptr, MAX_PTR);
	}

	if(last_ptr != -1) {
		if(ptr <= last_ptr) {
			warn("line %d: EOL address $%04x <= previous $%04x", lineno, ptr, last_ptr);
		}
	}

	if(lineno <= last_lineno) {
		warn("line number out of order (%d <= %d)", lineno, last_lineno);
	}

	if(lineno > MAX_LINENO) {
		warn("line number out range (%d > %d)", lineno, MAX_LINENO);
	}

	last_lineno = lineno;

	list_lineno(lineno);

	/* walk and print the tokens. when we hit a null byte, break out of the
	   loop, we're done with this line. */
	while(1) {
		tok = read_token(in_string || in_comment);

		if(in_string) {
			if(tok == 0x00) {
				/* null byte ends both the string and the line of code.
				   don't print a closing quote because AMSB doesn't. */
				break;
			} else if(tok == '|') {
				/* pipe is how AMSB stores the closing quote. end the string
				   but not the line of code, and print a " character. */
				in_string = 0;
				list_char('"');
			} else {
				/* normal string character. */
				list_char(tok);
				/* one " character embedded in a string gets printed as "" */
				if(tok == '"') list_char(tok);
			}
			continue;
		} else if(in_comment) {
			/* null byte ends both the comment and the line of code. */
			if(tok == 0x00) break;
			list_char(tok);
			continue;
		}

		if(was_colon) {
			if(tok != TOK_SQUOTE && tok != TOK_BANG && tok != TOK_ELSE) {
				list_char(':');
			}
			was_colon = 0;
		}

		if(tok == ':') {
			/* statement separator. don't print the colon yet, the next token
			   might be ELSE or a ! or ' for a comment */
			was_colon = 1;
		} else if(tok == '"') {
			/* begin string. strings start but *don't end* with a double-quote */
			in_string = 1;
			list_char(tok);
		} else if(tok >= 0x80) {
			if(!list_token(tok))
				warn("unknown %stoken %s$%02x at line %d",
						(tok > 0xff ? "extended " : ""),
						(tok > 0xff ? "$ff " : ""),
						tok & 0xff,
						lineno);
			if(is_comment_start(tok)) in_comment = 1;
		} else {
			/* null byte means the line of code is done */
			if(!tok) break;
			if(tok < 0x20) {
				/* ATASCII graphics outside of a string */
				warn("line %d has character %d outside of a string, maybe Atari BASIC?",
						lineno, tok);
			}
			if(tok == ' ') spacecount++;
			list_char(tok);
		}
	}

	len = bytes_read - offset;

	verbose(2, "      line %d length: %d", lineno, len);

	if(len > MAX_LINE_LEN) {
		int hard = len > MAX_LINE_LEN_HARD;
		warn("line %d is %s long (length %d > %d)",
			lineno,
			hard ? "impossibly" : "supiciously",
			len,
			hard ? MAX_LINE_LEN_HARD : MAX_LINE_LEN);
	}

	if(last_ptr != -1) {
		int plen = ptr - last_ptr;
		if(len != plen) {
			warn("line %d: EOL address doesn't match actual line length %d (should be %d)", lineno, len, plen);
		}
	}

	last_ptr = ptr;

	list_char(EOL);

	return 1;
}

/* when this gets called, input and output are open, read_header()
   has already run. "locking" and "unlocking" are the same
   transform, so this function does both.
   note that *no* checking of the program code is done here, so
   there's no need to finish() afterwards. */
void unlock_program(void) {
	int c;

	/* do not convert this to warn() (it ain't a warning) */
	fprintf(stderr, "%s: program is %slocked, output will be %slocked\n",
		self, locked ? "" : "un", locked ? "un" : "");

	/* 3-byte header: 0 for unlocked, 1 for locked */
	fputc(!locked, outfile);
	/* LSB of program length (not encrypted) */
	fputc(proglen & 0xff, outfile);
	/* MSB */
	fputc((proglen >> 8) & 0xff, outfile);

	/* rest of file, including trailing nulls, is transformed */
	while( (c = fgetc(infile)) >= 0)
		fputc(unlock_byte(c & 0xff), outfile);

	fclose(outfile);
	exit(0);
}

void write_code(int addr, int lineno, const unsigned char *code) {
	fputc(addr & 0xff, outfile);
	fputc((addr >> 8) & 0xff, outfile);
	fputc(lineno & 0xff, outfile);
	fputc((lineno >> 8) & 0xff, outfile);
	fputs((char *)code, outfile);
	fputc(0x00, outfile);
}

int crunch_line(void) {
	unsigned char code[MAX_LINE_LEN_HARD + 1], byte;
	int lineno, ptr, codelen = 0, in_string = 0, in_comment = 0, commentstart = 0;
	static int addr = 0x700; /* doesn't really matter where */

	ptr = read_prog_word();
	if(!ptr) return -1;
	lineno = read_prog_word();

	verbose(2, "crunching line %d", lineno);

	while(1) {
		if(codelen >= MAX_LINE_LEN_HARD)
			die("line %d too long, crunching failed", lineno);
		byte = read_prog_byte();

		if(byte != 0) {
			if(in_string) {
				if(byte == '|')
					in_string = 0;
			} else if(in_comment) {
				continue;
			} else {
				if(byte == '"')
					in_string = 1;
				else if(is_comment_start(byte)) {
					in_comment = 1;
					commentstart = codelen;
				}
				else if(byte == ' ')
					continue;
			}
		}

		code[codelen++] = byte;
		if(byte == 0) break;
	}

	/* omit comment-only lines */
	if(!keep_rems) {
		if(code[0] == TOK_REM) return 0;
		if(code[0] == ':') {
			if(code[1] == TOK_SQUOTE || code[1] == TOK_BANG)
				return 0;
		}
	}

	/* omit trailing comments */
	if(commentstart > 1) {
		code[commentstart - 1] = 0; /* null out the colon before the comment */
		codelen = commentstart;

		/* removing the comment from this: 10 PRINT :! BLAH
		   ...leaves a trailing colon. get rid of it if needed. */
		if(codelen >= 2) {
			if(code[codelen - 2] == ':') {
				code[codelen - 2] = 0;
				codelen--;
			}
		}
	}

	codelen += 4; /* account for ptr and lineno */
	addr += codelen;

	write_code(addr, lineno, code);

	return codelen;
}

int expand_token(int t, unsigned char *buf) {
	const char *result;

	if(t < 0x80) {
		buf[0] = t;
		buf[1] = 0;
		return 1;
	}

	if(t > 0xff) {
		t &= 0xff;
		if(t > MAX_EXT_TOK)
			return 0;
		result = ext_tokens[t - MIN_EXT_TOK];
	} else {
		if(t > MAX_STD_TOK)
			return 0;
		result = std_tokens[t - MIN_STD_TOK];
	}

	strcpy((char *)buf, result);
	return 1;
}

int read_token(int literal) {
	int tok = read_prog_byte();
	if(literal) return tok;
	if(tok == 0xff) {
		tok <<= 8;
		tok |= read_prog_byte();
	}
	return tok;
}

int token_is_extended(int tok) {
	return tok > 0xff;
}

int append_token(int tok, unsigned char *buf, int len) {
	if(token_is_extended(tok)) {
		buf[len++] = 0xff;
		buf[len++] = tok & 0xff;
	} else {
		buf[len++] = tok;
	}
	return len;
}

/* only called during decrunching.
	the tokenizer in AMSB requires spaces to separate keywords
   and variables/numbers. "IF A THEN 100" really needs the spaces
   before and after the A, for example. */
int need_space_between(int t1, int t2) {
	unsigned char text1[10], text2[10];
	unsigned char t1last, t2first;

	if(!t1) return 0; /* start of line */
	if(!t2) return 0; /* end of line */
	if(t1 < 0x80 && t2 < 0x80) return 0; /* 2 ASCII chars */

	if( !(expand_token(t1, text1) && expand_token(t2, text2)) )
		die("invalid token in program, decrunching failed");

	t1last = text1[strlen((char *)text1) - 1]; /* "PRINT" => "T" */
	t2first = text2[0];                        /* "PRINT" => "P" */

	/* if we already have a space, don't need to add another */
	if(t1last == ' ' || t2first == ' ') return 0;

	/* IF, THEN, and operators like AND/OR/NOT always get a
	   space after them, for readability. */
	switch(t1) {
		case TOK_IF:
		case TOK_THEN:
		case TOK_ELSE:
		case TOK_AND:
		case TOK_OR:
		case TOK_NOT:
			return 1;
		default: break;
	}

	/* these always get a space before them, for readability. */
	switch(t2) {
		case TOK_THEN:
		case TOK_AND:
		case TOK_OR:
		case TOK_NOT:
			return 1;
		default: break;
	}

	if(t1 >= 0x80 && isalnum(t1last)) {
		/* space not really required between OPEN/PRINT/CLOSE and #,
	      but put one there for neatness. */
		if(t2first == '#') return 1;
		/* same for POKE &52,0 or DATA &FF */
		if(t2first == '&') return 1;
		/* INPUT "PROMPT";A$ or DATA "FOO" */
		if(t2first == '"') return 1;
	}

	/* space not really required between a closing quote and
	   a keyword, but put it in for neatness. examples:
	     OPEN #1,"D:X" INPUT
	     IF A$="FOO" THEN 10
        PRINT "YOUR IQ IS" IQ
      these look weird without the space after the " */
	if(isalnum(t2first)) {
		if(t1last == '|') return 1;
		if(t1last == '$') return 1; /* OPEN #1,F$ OUTPUT */
	}

	return(isalnum(t1last) && isalnum(t2first));
}

int decrunch_line(void) {
	unsigned char code[MAX_LINE_LEN_HARD + 10];
	int lineno, tok = 0, prev, ptr, codelen = 0, in_string = 0, in_comment = 0;
	static int addr = 0x700; /* doesn't really matter where */

	ptr = read_prog_word();
	if(!ptr) return -1;
	lineno = read_prog_word();

	verbose(2, "decrunching line %d", lineno);

	while(1) {
		if(codelen >= MAX_LINE_LEN_HARD)
			die("line %d too long, decrunching failed", lineno);
		prev = tok;
		tok = read_token(in_string || in_comment);

		if(tok != 0) {
			if(in_string) {
				if(tok == '|')
					in_string = 0;
			} else if(in_comment) {
				/* NOP */
			} else {
				if(tok == ' ' || is_comment_start(tok)) {
					verbose(2, "found space/comment at line %d", lineno);
					spacecount++;
				}
				if(tok == '"') {
					in_string = 1;
					if(need_space_between(prev, tok))
						code[codelen++] = ' ';
				} else if(is_comment_start(tok)) {
					in_comment = 1;
				} else if(prev == ':' && tok == TOK_ELSE) {
					/* special case, ELSE needs a space before the (invisible)
					   colon that comes before it. */
					code[codelen - 1] = ' ';
					code[codelen++] = prev;
				} else if(need_space_between(prev, tok)) {
					code[codelen++] = ' ';
				}
			}
		}

		codelen = append_token(tok, code, codelen);
		if(tok == 0) break;
	}

	codelen += 4; /* account for ptr and lineno */
	addr += codelen;

	write_code(addr, lineno, code);

	return codelen;
}

/* despite the name, this handles both crunching and decrunching */
void crunch_program(void) {
	int newproglen = 0, linelen = 0;
	float percent;

	fputc(0x00, outfile); /* signature (0 = not locked) */
	fputc(0x00, outfile); /* length LSB (fill in later) */
	fputc(0x00, outfile); /* length MSB " "             */

	while((linelen = decrunch ? decrunch_line() : crunch_line()) != -1)
		newproglen += linelen;

	/* trailing $00 $00 */
	fputc(0x00, outfile);
	fputc(0x00, outfile);

	/* fill in length in header */
	if(fseek(outfile, 1L, SEEK_SET) < 0) os_err("fseek() failed");
	newproglen += 2; /* account for trailing $00 $00 */
	fputc(newproglen & 0xff, outfile);
	fputc((newproglen >> 8) & 0xff, outfile);
	fclose(outfile);

	/* show the user how the size changed */
	newproglen += 3;
	percent = 100.0 * (1.0 - (float)newproglen / (float)proglen);
	verbose(1, "%scrunched %d byte program to %d bytes (%.1f%% %s)",
			decrunch ? "de" : "",
			bytes_read,
			newproglen,
			percent < 0 ? -percent : percent,
			percent < 0 ? "larger" : "smaller");

	if(decrunch && spacecount)
		warn("%d spaces/comments, are you sure it was crunched?", spacecount);
}

void print_help(void) {
	printf("%s v" VERSION " - detokenize Atari Microsoft BASIC files\n", self);
	puts("By B. Watson <urchlay@slackware.uk>, released under the WTFPL");
	printf("Usage: %s [[-l] | [-[acClnviutms] ... ] [-r *start,end*]] <file> <outfile>\n", self);
	puts("  -a: raw ATASCII output");
	puts("  -c: check only (no listing)");
	puts("  -C: crunch program");
	puts("  -D: decrunch program");
	puts("  -l: lock or unlock program");
	puts("  -n: no blank line at start of listing");
	puts("  -v: verbosity");
	puts("  -r: only list lines numbered from *start* to *end*");
	puts("  --help, -h: print this help and exit");
	puts("  --version:  print version number and exit");
	puts("  -i -u -t -m -s: passed to a8cat (try 'a8cat -h')");
	puts("file must be a tokenized (SAVEd) AMSB file. if not given, reads from stdin.");
	puts("if outfile not given, writes to stdout (via pipe to a8cat)");
}

void version(void) {
	printf("%s " VERSION "\n", self);
}

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 > MAX_LINENO) {
				os_err("invalid line number for -r (range is 0-%d)", MAX_LINENO);
			}
		} else if(*p == ',' || *p == '-') {
			if(comma) os_err("invalid argument for -r (too many commas)");
			comma++;
			startline = val;
			val = 0;
		} else {
			if(comma) os_err("invalid argument for -r (only digits and comma allowed)");
		}
		p++;
	}

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

	if(endline < startline)
		os_err("invalid argument for -r (start > end)");
}

void parse_args(int argc, char **argv) {
	const char *a8cat;
	char tmp[10];
	int opt;

	a8cat = getenv("A8CAT");
	if(!a8cat) a8cat = "a8cat";

	strncpy(pipe_command, a8cat, BUFSIZ);

	if(argc >= 2) {
		if(strcmp(argv[1], "--help") == 0) {
			print_help();
			exit(0);
		} else if(strcmp(argv[1], "--version") == 0) {
			version();
			exit(0);
		}
	}

	while( (opt = getopt(argc, argv, "DCnLUlr:cvaiutmshk")) != -1) {
		switch(opt) {
			case 'L': crunch = decrunch = 0; break;
			case 'D': crunch = decrunch = 1; break;
			case 'C': crunch      = 1; break;
			case 'l':
			case 'U': unlock_mode = 1; break;
			case 'k': keep_rems   = 1; break;
			case 'c': check_only  = 1; break;
			case 'a': raw_output  = 1; break;
			case 'v': verbosity++    ; break;
			case 'n': initial_eol = 0; break;
			case 'h': print_help(); exit(0);
			case 'r': get_line_range(optarg); break;
			case 'i':
			case 'u':
			case 't':
			case 'm':
			case 's':
				if(strlen(pipe_command) > (BUFSIZ - 10))
					os_err("too many a8cat options");
				sprintf(tmp, " -%c", opt);
				strcat(pipe_command, tmp);
				break;
			default: print_help(); exit(1);
		}
	}

	if(optind >= argc) {
		if(isatty(fileno(stdin))) {
			print_help();
			os_err("can't read binary data from a terminal");
		}
		freopen(NULL, "rb", stdin);
		infile = stdin;
	} else {
		infile = fopen(argv[optind], "rb");
		if(!infile) {
			os_err("%s: %s", argv[optind], strerror(errno));
		}
	}

	optind++;
	if(optind < argc) {
		/* we were passed an output file */
		outname = argv[optind];
		if(check_only)
			os_err("can't use output filename %s with -c", outname);
		if(!freopen(outname, "wb", stdout))
			os_err("%s: %s", outname, strerror(errno));
		verbose(1, "redirected stdout to %s", outname);
	} else if(crunch) {
		os_err("can't use stdout with -C, must give an output filename");
	}
}

void open_output() {
	if(check_only) {
		outfile = freopen("/dev/null", "wb", stdout);
		if(!outfile) {
			os_err("/dev/null: %s", strerror(errno));
		}
		verbose(1, "using /dev/null for output (check_only)");
	} else if(raw_output || unlock_mode || crunch) {
		if(isatty(fileno(stdout))) {
			os_err("refusing to write %s to a terminal",
				(raw_output ? "raw ATASCII" : "tokenized BASIC"));
		}
		outfile = stdout;
		verbose(1, "using stdout for output");
	} else {
		const char *a8cat;
		if((a8cat = getenv("A8CAT"))) {
			verbose(1, "read A8CAT=%s from environment", a8cat);
		} else {
			verbose(1, "A8CAT not set in environment, using 'a8cat'");
		}
		verbose(1, "using pipe for output: %s", pipe_command);
		outfile = popen(pipe_command, "w"); /* "wb" not allowed! */
		/* we probably never see this error. popen() fails only if
		   we feed it an invalid mode, or if fork() or pipe() fails,
		   or I suppose if some idjit has done a "rm -f /bin/sh"...
		   all other errors are caught at pclose(). */
		if(!outfile) os_err("|%s: %s", pipe_command, strerror(errno));
		need_pclose = 1;
	}
}

int main(int argc, char **argv) {
	set_self(argv[0]);

	parse_args(argc, argv);

	open_output();

	read_header();

	if(unlock_mode) {
		unlock_program();
		exit(0); /* don't need finish() here, no parsing done */
	} else if(crunch) {
		crunch_program();
		exit(0);
	}

	start_listing();

	while(next_line())
		linecount++;

	finish(0);
	return 0; /* never executes; shuts up gcc warning */
}