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

#include "get_address.h"
#include "xex.h"

#ifndef VERSION
#define VERSION "???"
#endif

#define SELF "xexcat"
#define OPTIONS "hvo:l:r:i:cf:a1"

char *usage =
	SELF " v" VERSION " - by B. Watson (WTFPL)\n"
	"Join one or more Atari 8-bit executables into one multi-segment file.\n"
	"usage: " SELF " -[hvc] [-f 1|2 ] [-l address] [-i address] [-r address]\n"
	"              [-o outfile.xex] [infile1.xex] [infile2.xex ...]\n"
	"  -o outfile.xex  Output file (default: standard output)\n"
	"  -c              Check only; no output (same as -v -o/dev/null)\n"
	"  -l address      Force first load address (decimal, $hex, or 0xhex)\n"
	"  -i address      Force first init address\n"
	"  -r address      Force run address\n"
	"  -a              Force run address to load address of 1st segment.\n"
	"  -1              Output is an Atari DOS 1.0 executable, not xex.\n"
	"  -f 1|2          Input is DASM -f1 or -f2 format, not xex.\n"
	"  -h              Print this help\n"
	"  -v              Verbose operation\n";

static int readaddr(FILE *f) {
	int i, j;

	if( (i = fgetc(f)) < 0 )
		return -1;
	if( (j = fgetc(f)) < 0 )
		return -1;

	return i | (j << 8);
}

/* byte 0 = LSB of load address, byte 1 = MSB, rest = data. */
int read_f1(xex_segment *seg, FILE *f) {
	int i, addr, c;

	addr = readaddr(f);
	if(addr < 0)
		return 0;

	seg->start_addr = addr;
	for(i = 0; (i < 65536) && ((c = fgetc(f)) >= 0); i++) {
		seg->object[i] = c;
		seg->len++;
	}

	/* guard against the file being >64K length */
	fseek(f, 0, SEEK_END);

	seg->end_addr = seg->start_addr + seg->len - 1;
	seg->has_ff_header = 1;

	return (i != 0);
}

/* -f2 is segmented, each segment starts with a 4-byte header:
	0/1 are LSB/MSB of start address, 2/3 are LSB/MSB of length. */
int read_f2(xex_segment *seg, FILE *f) {
	int i, addr, c;

	addr = readaddr(f);
	if(addr < 0)
		return 0;

	seg->start_addr = addr;

	addr = readaddr(f);
	if(addr < 0)
		return 0;
	seg->len = addr;

	for(i = 0; (i < seg->len) && ((c = fgetc(f)) >= 0); i++) {
		if(c < 0) return 0;
		seg->object[i] = c;
	}

	seg->end_addr = seg->start_addr + seg->len - 1;
	seg->has_ff_header = 1;
	xex_print_seg_info(seg);
	return 1;
}

int read_seg(xex_segment *seg, FILE *f, int type) {
	switch(type) {
		case 1:
			return read_f1(seg, f);

		case 2:
			return read_f2(seg, f);

		case 0:
		default:
			return xex_fread_seg(seg, f);
	}
}

int main(int argc, char **argv) {
	xex_segment seg;
	char *outfile = "-";
	FILE *in, *out = stdout;
	int count = 1, c, errcount = 0;
	unsigned char buffer[65536]; /* be lazy: statically allocate large buffer */
	int force_load = -1, force_run = -1, force_init = -1;
	int read_stdin = 0, run_1st_seg = 0;
	int input_type = 0; /* 0 = xex, 1 and 2 are dasm -f1/-f2. */
	int dos1_output = 0;

	/* parse args */
	while( (c = getopt(argc, argv, OPTIONS)) > 0) {
		switch(c) {
			case 'h':
				printf(usage);
				exit(0);
				break;

			case 'v':
				xex_verbose = 1;
				break;

			case 'o':
				outfile = optarg;
				break;

			case 'c':
				xex_verbose = 1;
				outfile = "/dev/null";
				break;

			case 'l':
				if( (force_load = get_address(SELF, optarg)) < 0 )
					exit(1);
				break;

			case 'r':
				if( (force_run = get_address(SELF, optarg)) < 0 )
					exit(1);
				break;

			case 'i':
				if( (force_init = get_address(SELF, optarg)) < 0 )
					exit(1);
				break;

			case 'f':
				input_type = atoi(optarg);
				if(input_type < 1 || input_type > 2) {
					fprintf(stderr, SELF ": invalid -f argument (must be 1 or 2).\n");
					exit(1);
				}
				break;

			case 'a':
				run_1st_seg = 1;
				break;

			case '1':
				dos1_output = 1;
				break;

			default:
				fprintf(stderr, usage);
				exit(1);
		}
	}

	/* special case if there are no input filenames */
	if(argc <= optind) {
		read_stdin = 1;
	}

	/* open outfile */
	if(strcmp(outfile, "-") != 0) {
		if( !(out = fopen(outfile, "wb")) ) {
			fprintf(stderr, SELF ": %s: %s\n", outfile, strerror(errno));
			exit(1);
		}
	} else if(isatty(fileno(out))) {
		/* be polite... */
		fprintf(stderr,
				SELF ": Standard output is a terminal; not writing binary data\n"
				"Run '" SELF " -h' for help\n");
		exit(1);
	} else {
		outfile = "(standard output)";
	}

	/* only have to set seg.object once... */
	seg.object = buffer;

	/* process each input file on the command line */
	while(read_stdin || (optind < argc)) {
		char *infile = argv[optind++];
		int filecount = 1;

		if(read_stdin || strcmp(infile, "-") == 0) {
			read_stdin = 0;
			in = stdin;
			infile = "(standard input)";
		} else if( !(in = fopen(infile, "rb")) ) {
			/* failure to open is NOT fatal (just skip it and move on) */
			fprintf(stderr, SELF ": %s: %s\n", infile, strerror(errno));
			errcount++;
			continue;
		}

		if(xex_verbose)
			fprintf(stderr, SELF ": reading from file %s\n", infile);

		/* process every segment in current input file */
		while(read_seg(&seg, in, input_type)) {
			if(filecount++ == 1 && !seg.has_ff_header) {
				fprintf(stderr, SELF ": warning: '%s' first segment "
						"lacks $FFFF header (bad/partial XEX file?)\n",
						infile);
			}

			if(run_1st_seg && (force_run == -1)) {
				force_run = seg.start_addr;
				if(xex_verbose)
					fprintf(stderr,
							SELF ": %s: setting run address to %04X "
							"due to -a option\n",
							infile, force_run);
			}

			/* normalize the $FFFF headers: only the first segment needs one.
			   though if the output is DOS 1.0, it's $0984 instead. */
			if(count == 1) {
				if(!seg.has_ff_header) {
					if(seg.start_addr == 0x0984) {
						fprintf(stderr,
								SELF ": %s: can't handle DOS 1.0 executables as input\n"
								"  Convert this file to a regular .xex with xex1to2.\n",
								infile);
						errcount++;
						continue;
					}
				}
				if(dos1_output) {
					seg.has_ff_header = 0;
					fputc(0x84, out);
					fputc(0x09, out);
				} else {
					seg.has_ff_header = 1;
				}
			}

			/* process -l option */
			if(count == 1 && force_load > -1) {
				if(xex_verbose)
					fprintf(stderr,
							SELF ": %s: setting first load address to %04X "
							"due to -l option\n",
							infile, force_load);
				seg.start_addr = force_load;
				seg.end_addr = force_load + seg.len - 1;
				force_load = -1;
			}

			if(count == 1 && xex_get_init_addr(&seg) != -1) {
				fprintf(stderr, SELF ": %s: warning: first segment has init address, probably bogus.\n",
						infile);
			}

			count++;

			/* process -i option */
			if(seg.start_addr == XEX_INITAD && seg.len == 2) {
				if(dos1_output) {
					fprintf(stderr,
							SELF ": %s: "
							"warning: found init segment, but DOS 1.0 doesn't support them.\n",
							infile);
				}
				if(force_init == 0) {
					if(xex_verbose)
						fprintf(stderr,
								SELF ": %s: "
								"skipping first init address segment due to -i0\n",
								infile);
					force_init = -1;
					continue;
				} else if(force_init > 0) {
					if(xex_verbose)
						fprintf(stderr,
								SELF ": %s: "
								"setting first init address to %04X due to -i option\n",
								infile, force_init);
					seg.object[0] = XEX_LSB(force_init);
					seg.object[1] = XEX_MSB(force_init);
					force_init = -1;
				}

				if(xex_verbose)
					fprintf(stderr,
							SELF ": %s: init address: %04X\n",
							infile, XEX_ADDR(seg.object[0], seg.object[1]));
			}

			/* process -r option */
			if(seg.start_addr == XEX_RUNAD && seg.len == 2) {
				if(force_run > -1) {
					if(xex_verbose)
						fprintf(stderr,
								SELF ": %s: "
								"skipping run address segment due to -r option\n",
								infile);
					continue;
				} else {
					if(xex_verbose)
						fprintf(stderr,
								SELF ": %s: "
								"run address: %04X\n",
								infile, XEX_ADDR(seg.object[0], seg.object[1]));
				}
			}

			/* write (possibly modified) segment to output */
			if(!xex_fwrite_seg(&seg, out)) {
				fprintf(stderr, SELF ": %s: %s\n",
						outfile, xex_strerror(xex_errno));
				xex_errno = 0;
				errcount++;
				break;
			}
		}

		/* xex_errno will be 0 for XERR_NONE (meaning "no error") */
		if(xex_errno) {
			fprintf(stderr, SELF ": %s: %s\n",
					infile, xex_strerror(xex_errno));
			errcount++;
		} else if(filecount == 1) {
			fprintf(stderr, SELF ": warning: %s: file is empty.\n", infile);
		}

		if(xex_verbose)
			fprintf(stderr, SELF ": done reading file %s\n", infile);

		fclose(in);
	}

	/* if -r given, all run addresses in all files were omitted from the
		output file. Here we add a single run address to the output. */
	if(force_run > 0) {
		xex_run_seg(&seg, buffer, force_run);
		if(!xex_fwrite_seg(&seg, out)) {
			fprintf(stderr, SELF ": %s: %s\n",
					outfile, xex_strerror(xex_errno));
			errcount++;
		}
		count++;
	} else if(count == 1) {
		fprintf(stderr, SELF ": warning: %s: file is empty.\n", outfile);
	}

	if(xex_verbose)
		fprintf(stderr, SELF ": wrote %d segment%s to %s\n",
				(count - 1),
				(count == 2 ? "" : "s"),
				outfile);

	fclose(out);

	if(xex_verbose || errcount) {
		fprintf(stderr,
				SELF ": %d error%s.\n", errcount, (errcount == 1 ? "" : "s"));
		return errcount;
	}

	return 0;
}