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

#include "xex.h"

int xex_errno;
static int xex_sys_errno;

int xex_verbose = 0;

static char *errors[] = {
	"OK",                                 /* XERR_NONE      0 */
   "Failed system call",                 /* XERR_SYSCALL   1 */
   "End address < start address",        /* XERR_REVERSED  2 */
   "Truncated segment",                  /* XERR_TRUNCATED 3 */
   "No data",                            /* XERR_NULLDATA  4 */
   "Unknown/Invalid XEX error code",     /* XERR_MAXERR    5 */
};


int xex_new_seg(xex_segment *seg, unsigned char *data) {
	int offset = 0;

	if(data[0] == 0xff && data[1] == 0xff) {
		offset = 2;
		seg->has_ff_header = 1;
	} else {
		seg->has_ff_header = 0;
	}

	seg->start_addr = data[offset] + (data[offset + 1] << 8);
	offset += 2;

	seg->end_addr = data[offset] + (data[offset + 1] << 8);
	offset += 2;

	seg->object = &data[offset];

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

	return xex_check_seg(seg);
}

int xex_init_seg(xex_segment *seg, unsigned char *object, unsigned short addr) {
	object[0] = XEX_LSB(addr);
	object[1] = XEX_MSB(addr);

	seg->start_addr = XEX_INITAD;
	seg->end_addr = XEX_INITAD + 1;
	seg->object = object;
	seg->len = 2;
	seg->has_ff_header = 0;

	if(xex_verbose) {
		fprintf(stderr, "xex_init_seg(): created init segment @ %04X:\n", addr);
		xex_print_seg_info(seg);
	}

	return 1;
}

int xex_run_seg(xex_segment *seg, unsigned char *object, unsigned short addr) {
	object[0] = XEX_LSB(addr);
	object[1] = XEX_MSB(addr);

	seg->start_addr = XEX_RUNAD;
	seg->end_addr = XEX_RUNAD + 1;
	seg->object = object;
	seg->len = 2;
	seg->has_ff_header = 0;

	if(xex_verbose) {
		fprintf(stderr, "xex_run(): created run segment @ %04X:\n", addr);
		xex_print_seg_info(seg);
	}

	return 1;
}

static int read_char(FILE *file) {
	int c;

	c = getc(file);
	if(c == EOF) {
		if(ferror(file)) {
			xex_sys_errno = errno;
			xex_errno = XERR_SYSCALL;
		} else {
			xex_errno = XERR_TRUNCATED;
		}
	}

	return c;
}

int xex_fread_seg_header(xex_segment *seg, FILE *file) {
	int c, d;
	unsigned short addr;

	xex_errno = XERR_NONE;

	seg->has_ff_header = 0;
	seg->object = NULL;

	c = read_char(file);
	if(c == EOF) {
		if(feof(file))
			xex_errno = XERR_NONE;

		return 0;
	}
	d = read_char(file);
	if(d == EOF) return 0;

	addr = XEX_ADDR(c, d);
	if(addr == 0x0984) {
		fprintf(stderr, "xex_fread_seg_header(): warning: this might be a DOS 1.0 file (not supported)\n");
	}

	if(addr == 0xffff) {
		seg->has_ff_header = 1;
		c = read_char(file);
		if(c == EOF) return 0;
		d = read_char(file);
		if(d == EOF) return 0;
		addr = XEX_ADDR(c, d);
	}

	if(addr == 0xffff) {
		xex_errno = XERR_REVERSED;
		return 0;
	}

	seg->start_addr = addr;

	c = read_char(file);
	if(c == EOF) return 0;
	d = read_char(file);
	if(d == EOF) return 0;
	addr = XEX_ADDR(c, d);
	seg->end_addr = addr;

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

	if(seg->end_addr < seg->start_addr) {
		xex_errno = XERR_REVERSED;
		return 0;
	}

	if(xex_verbose) {
		fprintf(stderr, "xex_fread_seg_header(): read header:\n");
		xex_print_seg_info(seg);
	}

	return 1;
}

int xex_fread_seg_data(xex_segment *seg, FILE *file) {
	int res;

	xex_errno = XERR_NONE;

	res = fread(seg->object, 1, seg->len, file);
	xex_sys_errno = errno;

	if(xex_verbose) {
		fprintf(stderr, "xex_fread_seg_data(): read data, len==%d, res==%d:\n", seg->len, res);
		xex_print_seg_info(seg);
	}

	if(res == seg->len) {
		return 1;
	}

	if(res < 0 || ferror(file)) {
		xex_errno = XERR_SYSCALL;
	} else {
		/* EOF or short read */
		xex_errno = XERR_TRUNCATED;
		seg->len = res;
	}

	return xex_check_seg(seg);
}

int xex_fread_seg(xex_segment *seg, FILE *file) {
	unsigned char *tmp = seg->object;
	int res;

	xex_errno = XERR_NONE;

	if(tmp == NULL) {
		if(xex_verbose)
			fprintf(stderr, "xex_fread_seg(): seg->object == NULL\n");
		xex_errno = XERR_NULLDATA;
		return 0;
	}

	res = xex_fread_seg_header(seg, file);
	seg->object = tmp;

	if(!res)
		return 0;

	if(!xex_fread_seg_data(seg, file))
		return 0;

	return 1;
}

int xex_fwrite_seg(xex_segment *seg, FILE *file) {
	int res;

	xex_errno = XERR_NONE;

	if(xex_verbose) {
		fprintf(stderr, "xex_fwrite_seg(): about to write:\n");
		xex_print_seg_info(seg);
	}

	if(!xex_check_seg(seg))
		return 0;

	if(seg->has_ff_header) {
		putc(0xff, file);
		putc(0xff, file);
	}

	putc(XEX_LSB(seg->start_addr), file);
	putc(XEX_MSB(seg->start_addr), file);
	putc(XEX_LSB(seg->end_addr), file);
	putc(XEX_MSB(seg->end_addr), file);

	res = fwrite(seg->object, 1, seg->len, file);
	if(res == seg->len)
		return 1;

	if(ferror(file)) {
		xex_errno = XERR_SYSCALL;
		xex_sys_errno = errno;
	} else {
		xex_errno = XERR_TRUNCATED;
	}

	return 0;
}

int xex_get_object(xex_segment *seg, unsigned char *data) {
	int offset = 0;

	if(!xex_check_seg(seg))
		return 0;

	if(seg->has_ff_header) {
		data[0] = data[1] = 0xff;
		offset = 2;
	}

	data[offset] = XEX_LSB(seg->start_addr);
	data[offset + 1] = XEX_MSB(seg->start_addr);
	offset += 2;
	data[offset] = XEX_LSB(seg->end_addr);
	data[offset + 1] = XEX_MSB(seg->end_addr);
	offset += 2;

	memcpy(&data[offset], seg->object, seg->len);

	return 1;
}

int xex_check_seg(xex_segment *seg) {
	int ret = 1;

	xex_errno = XERR_NONE;

	if(seg->end_addr < seg->start_addr) {
		xex_errno = XERR_REVERSED;
		ret = 0;
	}

	if(seg->len != (seg->end_addr - seg->start_addr) + 1) {
		xex_errno = XERR_TRUNCATED;
		ret = 0;
	}

	if(seg->object == NULL) {
		xex_errno = XERR_NULLDATA;
		ret = 0;
	}

	if(xex_verbose && !ret && xex_errno != XERR_NULLDATA)
		fprintf(stderr, "xex_check_seg() FAILED: %s\n", xex_strerror(xex_errno));

	return ret;
}

static int xex_get_addr_from(xex_segment *seg, int location) {
	int i;

	if(seg->start_addr > location) return -1;
	if(seg->end_addr  <= location) return -1;

	i = location - seg->start_addr;
	return XEX_ADDR(seg->object[i], seg->object[i + 1]);
}

int xex_get_run_addr(xex_segment *seg) {
	return xex_get_addr_from(seg, XEX_RUNAD);
}

int xex_get_init_addr(xex_segment *seg) {
	return xex_get_addr_from(seg, XEX_INITAD);
}

char *xex_strerror(int err) {
	if(err < 0 || err >= XERR_MAXERR)
		err = XERR_MAXERR;

	if(err == XERR_SYSCALL)
		return strerror(xex_sys_errno);
	else
		return errors[err];
}

void xex_perror(char *msg) {
	fprintf(stderr, "%s: %s\n", msg, xex_strerror(xex_errno));
}

void xex_print_seg_info(xex_segment *seg) {
	xex_check_seg(seg);
	fprintf(stderr,
			"has_ff_header==%d  start==%04X  end==%04X  "
			"len==%04X  status==%s\n",
			seg->has_ff_header, seg->start_addr, seg->end_addr,
			seg->len, xex_strerror(xex_errno));
}