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

#include <sys/stat.h>
#include <sys/types.h>

/*
 * lump.c: manipulate the lumps in a Quake .bsp file (Currently only Q3/RTCW
 * are supported
 * Ben Winslow, Dec 9 2001
 */

#define IBSP 0x50534249 /* FIXME: q1 .bsp files don't have an IBSP header */
#define NUM_LUMPS 17	/* FIXME: Check version number for number of lumps */

#define MODE_EXPORT 0
#define MODE_IMPORT 1

static struct lump_t {
	off_t off;
	size_t len;
} lumps[NUM_LUMPS];

static struct file_t {
	unsigned char *buf;
	size_t len;
} *bsp;

/* Endian swapping functions */
static unsigned long lelong_get(unsigned char *);
static void lelong_put(unsigned char *, unsigned long);

/* File manipulation functions */
static struct file_t *file_read(char *);
static int file_write(struct file_t *, char *);
static void file_free(struct file_t *);

/* Functions to update the lumps struct and the bsp in memory */
static void lump_import(int, unsigned char *, size_t);
static int lump_export(int, char *);

/* Prints usage information */
static void usage(char *);

int main(int argc, char *argv[])
{
	char *infile = NULL, *outfile = NULL, *bspfilename;
	struct file_t *ents = NULL;
	int x, mode = MODE_EXPORT, lump = 0, opt;
	
	while ((opt = getopt(argc, argv, "i:en:o:h")) != -1) {
		switch (opt) {
			case 'i':
				infile = optarg;
				mode = MODE_IMPORT;
				break;
			case 'e':
				mode = MODE_EXPORT;
				break;
			case 'n':
				lump = atoi(optarg);
				if (lump > NUM_LUMPS) {
					printf("lump too high (NUM_LUMPS is %d)\n", NUM_LUMPS);
					return 1;
				}
				break;
			case 'o':
				outfile = strdup(optarg);
				break;
			case 'h':
			case '?':
			case ':':
				usage(argv[0]);
				return (int)((char)opt != 'h');
		}
	}
	if (argc - optind != 1) {
		usage(argv[0]);
		return 1;
	}
	bspfilename = argv[optind];
	
	/* load the .bsp file into memory */
	if (!(bsp = file_read(bspfilename))) {
		perror("file_read()");
		return 1;
	}
	if (lelong_get(bsp->buf) != IBSP)
		printf("warning: file does not have the 'IBSP' magic header\n");
	printf("BSP version %lu\n", lelong_get(&bsp->buf[4]));
	for (x = 0; x < NUM_LUMPS; x++) {
		lumps[x].off = (off_t)lelong_get(&bsp->buf[(x * 8) + 8]);
		lumps[x].len = (size_t)lelong_get(&bsp->buf[(x * 8) + 8 + 4]);
	}
	switch (mode) {
		case MODE_EXPORT:
			if (!outfile) {
				if (!(outfile = malloc(strlen(bspfilename) + 5))) {
					perror("malloc()");
					return 1;
				}
				sprintf(outfile, "%s.%.02d", bspfilename, lump);
			}
			fprintf(stderr, "Extracting lump %d to %s...\n", lump, outfile);
			if (lump_export(lump, outfile) < 0)
				fprintf(stderr, "lump_export(%d, %s): %s\n", lump, outfile, strerror(errno));
			free(outfile);
			break;
		case MODE_IMPORT:
			if (!infile) {
				fprintf(stderr, "A filename is required for the import operation!\n");
				break;
			}
			printf("Loading lump %d from %s...\n", lump, infile);
			if (!(ents = file_read(infile))) {
				perror("file_read()");
				break;
			}
			lump_import(lump, ents->buf, ents->len);
			if (!outfile)
				outfile = bspfilename;

			printf("Writing new BSP to %s\n", outfile);
			if (file_write(bsp, outfile) < 0)
				fprintf(stderr, "write_file(%s): %s\n", outfile ? outfile : bspfilename, strerror(errno));
			file_free(ents);
			break;
	}
	file_free(bsp);
	return 0;
}

static unsigned long lelong_get(unsigned char *buf)
{
	return (unsigned long)((buf[3] << 24) | (buf[2] << 16) | (buf[1] << 8) | buf[0]);
}

static void lelong_put(unsigned char *buf, unsigned long x)
{
	buf[3] = (unsigned char)((x >> 24) & 0xff);
	buf[2] = (unsigned char)((x >> 16) & 0xff);
	buf[1] = (unsigned char)((x >> 8) & 0xff);
	buf[0] = (unsigned char)(x & 0xff);
}

static struct file_t *file_read(char *filename)
{
	struct stat st;
	struct file_t *out;
	unsigned char *buf;
	int fd;
	ssize_t res;
	
	if (stat(filename, &st) < 0)
		return NULL;
	if ((fd = open(filename, O_RDONLY)) < 0)
		return NULL;
	if (!(buf = malloc((size_t)st.st_size)))
		return NULL;
	if ((res = read(fd, buf, (size_t)st.st_size)) < 0) {
		free(buf);
		return NULL;
	}
	(void)close(fd);

	if (!(out = malloc(sizeof(struct file_t)))) {
		free(buf);
		return NULL;
	}
	out->buf = buf;
	out->len = (size_t)res;
	
	return out;
}

static int file_write(struct file_t *f, char *filename)
{
	int fd;
	
	if ((fd = open(filename, O_WRONLY|O_CREAT|O_TRUNC, 0666)) < 0)
		return -1;
	if (write(fd, f->buf, f->len) < 0)
		return -2;
	if (close(fd) < 0)
		return -3;
	return 0;
}

static void file_free(struct file_t *f)
{
	if (f) {
		free(f->buf);
		free(f);
	}
}

static void lump_import(int num, unsigned char *buf, size_t len)
{
	size_t newlen;
	int x;
	
	bsp->buf = realloc(bsp->buf, bsp->len + (len - lumps[num].len));
	newlen = bsp->len + (len - lumps[num].len);
	
	/*
	 * Offset the data after the lump we're changing
	 * (we don't want to clobber it)
	 * FIXME: This is ugly
	 */
	 
	memmove(&bsp->buf[lumps[num].off + len], &bsp->buf[lumps[num].off + lumps[num].len], bsp->len - (lumps[num].off + lumps[num].len));
	/* Insert the new lump data */
	memcpy(&bsp->buf[lumps[num].off], buf, len);
	
	/* Fix the header */
	for (x = 0; x < 17; x++) {
		if (lumps[x].off > lumps[num].off)
			lumps[x].off += (len - lumps[num].len);
	}
	lumps[num].len = len;
	/* And write the header to the buffer again */
	for (x = 0; x < NUM_LUMPS; x++) {
		lelong_put(&bsp->buf[(x * 8) + 8], (unsigned long)lumps[x].off);
		lelong_put(&bsp->buf[(x * 8) + 8 + 4], (unsigned long)lumps[x].len);
	}
	bsp->len = newlen;
}

static int lump_export(int num, char *filename)
{
	int fd;
	
	if ((fd = open(filename, O_WRONLY|O_CREAT|O_TRUNC, 0666)) < 0)
		return -1;
	if (write(fd, &bsp->buf[lumps[num].off], lumps[num].len) < 0)
		return -2;
	if (close(fd) < 0)
		return -3;
	return 0;
}

static void usage(char *name)
{
	fprintf(stderr, "usage: %s [-i <filename>|-e] [-n <lump>] [-o <outfile>] <infile.bsp>\n", name);
	fprintf(stderr, "\t-i <filename>\tImport a filename into the specified lump\n");
	fprintf(stderr, "\t-e\t\tExport the specified lump (default)\n");
	fprintf(stderr, "\t-n <lump>\tSpecify the lump number to operate on (default: 0)\n");
	fprintf(stderr, "\t-o <outfile>\tSpecifies the output filename\n");
}
