/* Copyright 2013 Bernhard R. Link <brlink@debian.org>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * SOFTWARE IN THE PUBLIC INTEREST, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */
#include "config.h"

#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <assert.h>
#include <endian.h>
#include <errno.h>

#include "globals.h"
#define EFF_INTERNAL 1
#include "eff.h"

/* some trivial little extensible file format - reading support */

struct eff {
	char *filename;
	FILE *file;
	block_type typecount;
	struct efftype {
		block_type id;
		char *name;
	} *types;
	size_t depth;
	size_t arraylen;
	size_t arraycount;
};

static void eff_read(void *buffer, size_t len, struct eff *file) {
	ssize_t r;

	if (file->depth == 0)
		fail("Internal error: trying to read past EOF marker.");

	r = fread(buffer, len, 1, file->file);
	if (r < 0)
		fail("Error %d reading '%s': %s", errno, file->filename,
				strerror(errno));
	if (r != 1)
		fail("Unexpectedend of file '%s'", file->filename);
}

struct eff *eff_open(const char *filename, const char *type) {
	struct eff *f;
	size_t typelen = strlen(type);
	unsigned char buffer[5];
	size_t recordedtypelen;
	char *typebuffer;

	f = zNEW(1, struct eff);
	f->depth = 1;
	f->filename = strdup(filename);
	if (f->filename == NULL)
		oom();
	f->file = fopen(filename, "r");
	if (f == NULL)
		fail("Error %d opening '%s': %s",
				errno, filename, strerror(errno));
	errno = 0;
	eff_read(buffer, 5, f);
	if (memcmp(buffer, "eff", 3) != 0)
		fail("File '%s' has unrecognized file format!", filename);
	if (buffer[3] != 0 || buffer[4] == 0)
		fail("File '%s' in not yet supported extensible file format version",
				filename);
	recordedtypelen = buffer[4];
	typebuffer = alloca(recordedtypelen + 1);
	eff_read(typebuffer, recordedtypelen, f);
	typebuffer[recordedtypelen] = '\0';
	if (recordedtypelen != typelen || memcmp(type, typebuffer, typelen) != 0) {
		fail("File '%s' has not recorded purpose '%s' but '%s'!",
				f->filename, type, typebuffer);
	}
	f->typecount = BT_BLOCKSTART;
	return f;
}

static inline block_type getblocktype(struct eff *file) {
	uint8_t type;
	uint8_t len;
	struct efftype *t;

	assert (file->arraylen == 0 && file->arraycount == 0);

	while (true) {
		eff_read(&type, 1, file);
		/* if (type >= 0x80) ... */
		if (type >= file->typecount)
			fail("Malformed file '%s' (unexpected typeid)",
					file->filename);
		if (type != BT_TYPE)
			return type;
		if (file->typecount >= BT_MAX) {
			fail("File '%s' uses too new file format (too many types)",
					file->filename);
		}
		eff_read(&len, 1, file);
		RESIZE(&file->types,
				file->typecount + 1 - BT_BLOCKSTART,
				struct efftype);
		t = &file->types[file->typecount - BT_BLOCKSTART];
		t->id = file->typecount++;
		t->name = malloc(len + 1);
		if (t->name == NULL)
			oom();
		eff_read(t->name, len, file);
		t->name[len] = 0;
	}
}

const char *eff_start(struct eff *file) {
	block_type t = getblocktype(file);
	if (t == BT_END) {
		file->depth--;
		return NULL;
	}
	if (t >= BT_BLOCKSTART) {
		file->depth++;
		return file->types[t - BT_BLOCKSTART].name;
	}
	fail("File '%s' not containing block where on expected", file->filename);
}

void eff_fixedstart(struct eff *file, const char *type) {
	const char *t = eff_start(file);

	if (t == NULL || strcmp(t, type) != 0)
		fail("Malformed file '%s': No block of type '%s' where one was expected (instead got %s)",
				file->filename, type, t);
}

void eff_end(struct eff *file) {
	block_type t = getblocktype(file);
	if (t != BT_END) {
		fail("Malformed file '%s': unexpected data where end of block was expected",
				file->filename);
	}
	file->depth--;
}

uint32_t eff_getuint32(struct eff *file) {
	block_type t = getblocktype(file);
	uint32_t buffer;

	if (t != BT_UINT32) {
		fail("Malformed file '%s': unexpected data where uint32 was expected",
				file->filename);
	}
	eff_read(&buffer, 4, file);
	return u32trans(buffer);
}
uint16_t eff_getuint16(struct eff *file) {
	block_type t = getblocktype(file);
	uint16_t buffer;

	if (t != BT_UINT16) {
		fail("Malformed file '%s': unexpected data where uint16 was expected",
				file->filename);
	}
	eff_read(&buffer, 2, file);
	return u16trans(buffer);
}
uint8_t eff_getuint8(struct eff *file) {
	block_type t = getblocktype(file);
	uint8_t buffer;

	if (t != BT_UINT8) {
		fail("Malformed file '%s': unexpected data where uint8 was expected",
				file->filename);
	}
	eff_read(&buffer, 1, file);
	return buffer;
}

char *eff_getstring(struct eff *file) {
	block_type t = getblocktype(file);
	uint8_t len;
	char *s;

	if (t != BT_SHORTSTRING) {
		fail("Malformed file '%s': unexpected data where string was expected",
				file->filename);
	}
	eff_read(&len, 1, file);
	s = malloc(len + (size_t)1);
	if (s == NULL)
		oom();
	if (len > 0)
		eff_read(s, len, file);
	s[len] = '\0';
	return s;
}

void eff_openarray(struct eff *file, size_t len, size_t *num_p) {
	block_type t = getblocktype(file);
	uint8_t recordedlen;
	uint16_t recordednum;

	if (t != BT_SHORTARRAY) {
		fail("Malformed file '%s': unexpected data where array was expected",
				file->filename);
	}
	eff_read(&recordedlen, 1, file);
	eff_read(&recordednum, 2, file);
	recordednum = u16trans(recordednum);
	if (len != recordedlen)
		fail("Malformed file '%s': array element length mismatch",
				file->filename);
	file->arraylen = len;
	file->arraycount = recordednum;
	*num_p = recordednum;
}

void eff_getelement(struct eff *file, size_t len, void *data) {
	assert (file->arraycount > 0);
	assert (file->arraylen == len);

	eff_read(data, len, file);
	file->arraycount--;
}

void eff_closearray(struct eff *file) {
	assert (file->arraycount == 0);
	assert (file->arraylen != 0);
	file->arraylen = 0;
}

void eff_close(struct eff *file) {
	if (file->depth > 0)
		eff_end(file);
	if (ferror(file->file))
		fail("There were errors reading '%s'", file->filename);
	if (fclose(file->file) != 0)
		fail("There were errors reading '%s'", file->filename);
	while (file->typecount > BT_BLOCKSTART) {
		free(file->types[--file->typecount - BT_BLOCKSTART].name);
	}
	free(file->types);
	free(file->filename);
	free(file);
}
