#include "unalf.h"
#include "addrs.h"

static int headers_read = 0;
static int convert_eols = 0;

static void die_arc(void) {
	fprintf(stderr, "%s: fatal: this is an ARC file, not ALF\n", self);
	exit(1);
}

static void die_not_alf(void) {
	fprintf(stderr, "%s: fatal: not an ALF file\n", self);
	exit(1);
}

/* truncating the file by name could be a race condition, but I don't
   think it's going to be a real-world problem for anyone. */
static void eof_junk(long pos) {
	fprintf(stderr, "%s: junk at EOF (%s)\n", self, opts.fixjunk ? "removing" : "ignoring");
	if(opts.fixjunk) {
		if(truncate(in_filename, pos) < 0) {
			fprintf(stderr, "%s: could not remove junk: ", self);
			perror(in_filename);
		}
	}
}

static void check_hdr_size(const char *name, unsigned long size) {
	const char *desc;
	int fatal = 0;

	/* fits on a double-density disk? */
	if(size < 184320)
		return;

	/* >= 16MB files are impossible because the decrunch algorithm
		ignores the high byte of the 4-byte length. */
	if(size >= 16777216L) {
		desc = "impossibly large (>=16MB)";
		fatal = !opts.listonly; /* listing the file isn't an error, extracting it is. */
	/* >1MB files are possible (e.g. with a hard drive on SpartaDOS X)
		but exceedingly rare in the Atari world. */
	} else if(size > 1048576L) {
		desc = "suspiciously large (>1MB)";
	} else {
		desc = "too large for a floppy disk (>180KB)";
	}

	fprintf(stderr, "%s: %s: header #%d %s size is %s.\n",
			self, (fatal ? "fatal" : "warning"), headers_read, name, desc);
	if(fatal) exit(1);
}

static void sanity_check_header(long pos) {
	struct stat s;
	unsigned long origsize, compsize;
	int fatal;

	origsize = getquad(alf_hdr_origsize0);
	compsize = getquad(alf_hdr_compsize0);

	check_hdr_size("original", origsize);
	check_hdr_size("compressed", compsize);

	pos += 29; /* skip header */
	if(fstat(fileno(in_file), &s) < 0) {
		fprintf(stderr, "%s: fatal: fstat on %s ", self, in_filename);
		perror("failed");
		return;
	}

	if(compsize > (s.st_size - pos)) {
		fatal = !(opts.force || opts.listonly);
		fprintf(stderr, "%s: %s: compressed size for header #%d is bigger than the rest of the file (truncated?)", self, fatal ? "fatal" : "warning", headers_read);
		if(fatal) {
			fputs(", use -F to override.\n", stderr);
			exit(1);
		}
		fputs("\n", stderr);
	}

	/* 0 byte input gives a 2-byte output, 1 byte input gives 4,
	   2 bytes gives 5.
	   don't complain about these. starting with any 3 byte input,
	   the compressed size will always be under 2x the input size. */
	if(origsize > 2 && compsize > 5 && compsize > origsize * 2) {
		fatal = !(opts.force || opts.listonly);
		fprintf(stderr, "%s: %s: compressed size for header #%d is over twice the uncompressed size (corrupt?)", self, fatal ? "fatal": "warning", headers_read);
		if(fatal) {
			fputs(", use -F to override.\n", stderr);
			exit(1);
		}
		fputs("\n", stderr);
	}
}

/* return 1 if a header is read, 0 if not */
int read_alf_header(void) {
	u8 h1, h2;
	int bytes;
	long read_pos;

	read_pos = ftell(in_file);

	bytes = fread(mem + alf_header, 1, 29, in_file);

	if(!bytes) {
		if(headers_read)
			return 0;
		else
			die_not_alf();
	} else if(bytes < 29) {
		if(headers_read) {
			eof_junk(read_pos);
			return 0;
		} else {
			die_not_alf();
		}
	}

	h1 = mem[alf_header];
	h2 = mem[alf_hdr_sig];

	if(h1 == 0x1a) {
		if(h2 < 0x0f) die_arc();
		if(h2 == 0x0f) {
			headers_read++;
			sanity_check_header(read_pos);
			return 1; /* signature matches */
		}
	}

	if(headers_read)
		eof_junk(read_pos);
	else
		die_not_alf();

	return 0;
}

/* read buf_len_l/h bytes into buf_adr_l/h, then store the number
   of bytes actually read in buf_len_l/h. EOF is handled like the
   Atari does: you get a partial buffer *and* EOF status. */
void readblock(void) {
	int bytes, len, bufadr;
	u8 *buf;

	bufadr = dpeek(buf_adr_l);
	buf = mem + bufadr;
	len = dpeek(buf_len_l);

	bytes = fread(buf, 1, len, in_file);

	/* mimic CIO's behaviour: Y=1 means OK, Y>=0x80 means error */
	if(feof(in_file)) {
		ldy_i(0x88);
	} else {
		ldy_i(1);
	}
	dpoke(buf_len_l, bytes);
}

/* Atari-specific (can't use isprint()) */
static int is_printable(u8 c) {
	return (c == 0x9b || (c >= ' ' && c <= 124));
}

static int is_text_file(u8 *buf) {
	return is_printable(buf[0]) && is_printable(buf[1]);
}

/* mirror of readblock(), plus EOL conversion if needed. With -a,
   a file is considered text if its first 2 bytes are printable ATASCII,
   including EOLs. With -aa, all files are converted. */
void writeblock(void) {
	int i, bytes, len, bufadr;
	u8 *buf;

	bufadr = dpeek(buf_adr_l);
	buf = mem + bufadr;
	len = dpeek(buf_len_l);

	if(new_file) {
		if(opts.txtconv > 1) {
			convert_eols = 1;
		} else if(opts.txtconv == 1 && len > 1) {
			convert_eols = is_text_file(buf);
		} else {
			convert_eols = 0;
		}
	}
	new_file = 0;

	if(convert_eols) {
		for(i = 0; i < len; i++) {
			if(buf[i] == 0x9b) buf[i] = '\n';
			if(buf[i] == 0x7f) buf[i] = '\t';
		}
	}

	bytes = fwrite(buf, 1, len, out_file);
	if(bytes < 0) {
		extern char *out_filename; /* extract.c */
		fprintf(stderr, "%s: fatal: ", self);
		perror(out_filename);
		exit(1);
	}
	bytes_written += bytes;
	dpoke(buf_len_l, bytes);
}
