#define __MILETOS_XFPK_CPP__

//
// Libmiletos
//
// Copyright (C) Lauris Kaplinski 2007-2009
//

static const int debug = 0;

#include <stdio.h>

#include <libarikkei/token.h>

#include "xfpk.h"

namespace Miletos {

namespace Xfpk {

#ifdef WIN32
inline char *strdup (const char *str) { return _strdup (str); }
#endif

// FileData

FileData::FileData (const char *ptblname, const char *pdataname)
: valid (0), tblname(ptblname), dataname(pdataname)
{
	size_t tsize;
	const unsigned char *tdata = Miletos::FileSystem::mmapFile (tblname.c_str (), &tsize);
	size_t csize;
	const unsigned char *cdata = Miletos::FileSystem::mmapFile (dataname.c_str (), &csize);
	if (tdata && cdata) {
		readTable (tdata, tsize, cdata, csize);
	}
	if (cdata) Miletos::FileSystem::munmapFile (cdata);
	if (tdata) Miletos::FileSystem::munmapFile (tdata);
	if (!entries.empty ()) valid = 1;
}

#if 0
FileData::FileData (const unsigned char *tdata, size_t tsize, const unsigned char *pcdata, size_t pcsize)
: valid (0), path("")
{
	if (!tdata || !cdata) return;
	readTable (tdata, tsize);
}
#endif

void
FileData::readTable (const unsigned char *tdata, size_t tsize, const unsigned char *cdata, size_t csize)
{
	size_t tblsize;
	unsigned char *tbl = unpack_a67f54cb (tdata, tsize, tblsize);
	if (!tbl) return;

	// FILE *ofs = fopen ("table.bin", "wb+");
	// fwrite (tbl, 1, tblsize, ofs);
	// fclose (ofs);

	int filenum = 0;
	size_t lastpos = 0;
	for (size_t dpos = 0; dpos < 0x800; dpos += 8) {
		Miletos::u32 dstart = getU32LE (tbl + dpos);
		Miletos::u32 dsize = getU32LE (tbl + dpos + 4);
		for (Miletos::u32 i = 0; i < dsize; i++) {
			Miletos::u32 pos = 0x800 + dstart + 80 * i;
			Miletos::u32 blockpos = getU32LE (tbl + pos + 4);
			Miletos::u32 blocksize = getU32LE (tbl + pos + 8);
			const char *blockname = (const char *) (tbl + pos + 16);
			if (!blocksize || (blocksize > (20 * 1024 * 1024))) {
				fprintf (stderr, "Block bigger than 20MiB (%d) - stop\n", blocksize);
				break;
			}
			if (blockpos < lastpos) break;
			if (!*blockname) break;
			FileEntry e;
			e.tag = getU32LE (cdata + blockpos);
			e.name = blockname;
			e.start = blockpos;
			e.length = blocksize;
			entries.push_back (e);
			map[e.name] = filenum;
			lastpos = blockpos;
			filenum += 1;
		}
	}
	free (tbl);
}

const char *
FileData::getFileName (int idx)
{
	if ((idx < 0) || (idx >= (int) entries.size ())) return NULL;
	return entries[idx].name.c_str ();
}

size_t
FileData::getFileSize (int idx)
{
	if ((idx < 0) || (idx >= (int) entries.size ())) return NULL;
	return entries[idx].length;
}

const unsigned char *
FileData::getFileData (int idx, size_t *size)
{
	if ((idx < 0) || (idx >= (int) entries.size ())) return NULL;

	size_t csize;
	const unsigned char *cdata = Miletos::FileSystem::mmapFile (dataname.c_str (), &csize);
	if (!cdata) {
		valid = 0;
		return NULL;
	}

	unsigned char *d = (unsigned char *) malloc (entries[idx].length);
	memcpy (d, cdata + entries[idx].start, entries[idx].length);
	if (size) *size = entries[idx].length;

	Miletos::FileSystem::munmapFile (cdata);

	return d;
}

Miletos::u32
FileData::getFileTag (int idx)
{
	if ((idx < 0) || (idx >= (int) entries.size ())) return NULL;
	return entries[idx].tag;
}

#define	ILLUSION_OBJN	(('O') | ('B' << 8) | ('J' << 16) | ('N' << 24))

bool
FileData::isCompressed (int idx)
{
	if ((idx < 0) || (idx >= (int) entries.size ())) return NULL;
	return entries[idx].tag == ILLUSION_OBJN;
}

unsigned char *
FileData::unpack_a67f54cb (const unsigned char *cdata, size_t csize, size_t& dsize)
{
	Miletos::u32 uncompressed_size = *((Miletos::u32 *) cdata) ^ 0xa67f54cb;
	if (uncompressed_size > (1024 * 1024 * 1024)) {
		if (debug > 1) fprintf (stderr, "FileData::unpack_a67f54cb: Invalid uncompressed block size %d (%x)\n", uncompressed_size, uncompressed_size);
		return NULL;
	}
	// u32 mval = *((u32 *) cdata + 1);
	// if (mval != 0xca27162f) return NULL;

	unsigned char *sbuf = (unsigned char *) malloc (csize);
	if (!sbuf) {
		fprintf (stderr, "FileData::unpack_a67f54cb: cannot allocate memory\n");
		return NULL;
	}
	memcpy (sbuf, cdata, csize);

	if (debug > 1) fprintf (stderr, "FileData::unpack_a67f54cb: Uncompressed table size: %d (%x)\n", uncompressed_size, uncompressed_size);
	unsigned char *dbuf = (unsigned char *) malloc (uncompressed_size);

	Miletos::u32 ssize = (Miletos::u32) csize;
	for (size_t i = 4; i < (csize - 3); i += 4) {
		*((Miletos::u32 *) (sbuf + i)) ^= 0x35d8ca2f;
	}

	Miletos::u32 spos = 4;
	Miletos::u32 mask = 0;
	Miletos::u32 dpos = 0;
	Miletos::u32 tblpos = 0xfee;
	unsigned char tbl[0xffff] = { 0 };

	while (spos < ssize) {
		mask = mask >> 1;
		if (!(mask & 0x100)) {
			// if ((spos + 1) >= ssize) break;
			mask = 0xff00 | sbuf[spos];
			spos += 1;
		}
		if (mask & 0x1) {
			// if ((spos + 1) >= ssize) break;
			// if (spos >= ssize) break;
			dbuf[dpos] = sbuf[spos];
			tbl[tblpos] = dbuf[dpos];
			tblpos += 1;
			dpos += 1;
			spos += 1;
			tblpos = tblpos & 0xfff;
		} else {
			Miletos::u32 location = sbuf[spos];
			spos += 0x1;
			// if ((spos + 1) >= ssize) break;
			if (spos >= ssize) break;
			Miletos::u32 length = sbuf[spos];
			location = ((length & 0xf0) << 4) | location;
			spos += 1;
			// if (spos >= ssize) break;
			length = (length & 0xf) + 0x2;
			for (Miletos::u32 i = 0; i <= length; i++) {
				dbuf[dpos] = tbl[(location + i) & 0xfff];
				tbl[tblpos] = dbuf[dpos];
				tblpos += 1;
				dpos += 1;
				tblpos = tblpos & 0xfff;
				if (dpos >= uncompressed_size) break;
			}
		}
		if (dpos >= uncompressed_size) break;
	}

	free (sbuf);

	if (dpos != uncompressed_size) {
		if (debug > 1) fprintf (stderr, "FileData::unpack_a67f54cb: uncompressed size differs %d (%x) != %d (%x)\n", (int) dsize, (int) dsize, uncompressed_size, uncompressed_size);
		// fixme: For some reason dpos == (uncompressed_size - 1)
		// return NULL;
	}

	dsize = dpos;
	return dbuf;
}

bool
FileData::isTable (const char *tblname)
{
	size_t tsize;
	const unsigned char *tdata = FileSystem::mmapFile (tblname, &tsize);
	if (!tdata) return false;

	size_t tblsize;
	unsigned char *tbl = unpack_a67f54cb (tdata, tsize, tblsize);
	if (!tbl) {
		FileSystem::munmapFile (tdata);
		return false;
	}

	// int filenum = 0;
	size_t lastpos = 0;
	size_t dpos = 0;
	for (dpos = 0; dpos < 0x800; dpos += 8) {
		u32 dstart = getU32LE (tbl + dpos);
		u32 dsize = getU32LE (tbl + dpos + 4);
		for (u32 i = 0; i < dsize; i++) {
			u32 pos = 0x800 + dstart + 80 * i;
			u32 blockpos = getU32LE (tbl + pos + 4);
			u32 blocksize = getU32LE (tbl + pos + 8);
			const char *blockname = (const char *) (tbl + pos + 16);
			if (!blocksize || (blocksize > (20 * 1024 * 1024))) {
				break;
			}
			if (blockpos < lastpos) break;
			// if ((blockpos + blocksize) > csize) break;
			if (!*blockname) break;
		}
	}
	free (tbl);
	Miletos::FileSystem::munmapFile (tdata);
	return (dpos >= 0x800);
}

// FileHandler

FileHandler::FileHandler (const char *tblname, const char *dataname)
: HandlerFileList(tblname), data(tblname, dataname)
{
	_valid = data.isValid ();
	int nfiles = data.getNumFiles ();
	for (int i = 0; i < nfiles; i++) {
		const char *fname = data.getFileName (i);
		size_t fnlen = strlen (fname);
		Entry *e = new Entry();
		e->id = i;
		e->name = (char *) malloc (fnlen + 2);
		e->name[0] = '/';
		memcpy (e->name + 1, fname, fnlen);
		e->name[fnlen + 1] = 0;
		e->dataref = 0;
		e->csize = 0;
		e->cdata = NULL;
		entries.push_back (e);
		namedict.insert (e->name, e);
	}
}

bool
FileHandler::hasDir (const char *name)
{
	if (!name) return false;
	// "" and "/" are only allowed directories
	if (!*name || !strcmp (name, "/")) return true;
	return false;
}

bool
FileHandler::loadDir (const char *name)
{
	if (!name) return false;
	// "" and "/" are only allowed directories
	if (!*name || !strcmp (name, "/")) return true;
	return false;
}

int
FileHandler::getNumFiles (void)
{
	return data.getNumFiles ();
}

const char *
FileHandler::getFileName (int idx)
{
	return data.getFileName (idx);
}

size_t
FileHandler::getFileSize (int idx)
{
	return data.getFileSize (idx);
}

int
FileHandler::getNumSubDirs (void)
{
	return 0;
}

const char *
FileHandler::getSubDirName (int idx)
{
	return NULL;
}

void
FileHandler::addFileMapping (Entry *entry)
{
	entry->cdata = (unsigned char *) data.getFileData (entry->id, &entry->csize);
}

// URLHandler

URLHandler::URLHandler (const char *location)
: URI::URLHandler(location)
{
	// syntax: xfpk:(table,data[;table,data...])/
	char *ploc = strdup (location + 6);
	ploc[strlen (ploc) - 2] = 0;
	Arikkei::TokenChar ltoken(ploc);
	Arikkei::TokenChar tokenz[16];
	int ntokenz = ltoken.tokenize (tokenz, 16, Arikkei::TokenChar(";"), false);
	for (int i = 0; i < ntokenz; i++) {
		Arikkei::TokenChar itokenz[3];
		int nitokenz = tokenz[i].tokenize (itokenz, 3, Arikkei::TokenChar(","), false);
		if (nitokenz != 2) {
			free (ploc);
			return;
		}
		FileHandler *handler = new FileHandler((const char *) itokenz[0], (const char *) itokenz[1]);
		handlers.push_back (handler);
	}
	free (ploc);
}

URLHandler::~URLHandler (void)
{
	for (size_t i = 0; i < handlers.size (); i++) {
		handlers[i]->unRef ();
	}
}

URI::URLHandler *
URLHandler::newURLHandler (const char *url)
{
	// syntax: xfpk:(table,data[;table,data...])/[file]
	if (!url || !*url) return NULL;
	URI::Location loc(url);
	if (strcmp ((const char *) loc.protocol, "xfpk")) return NULL;
	if (!loc.domain) return NULL;
	if (strcmp ((const char *) loc.directory, "/")) return NULL;
	return new URLHandler((const char *) loc.base);
}

const unsigned char *
URLHandler::mmapDataRelative (const char *name, size_t *size)
{
	if (!name || !*name) return NULL;
	for (size_t i = 0; i < handlers.size (); i++) {
		const unsigned char *data = handlers[i]->mmapFile (name, size);
		if (data) return data;
	}
	return NULL;
}

void
URLHandler::munmapData (const unsigned char *data)
{
	// XFPK data is owned by FileHandler
}

}; // Namespace Xfpk

}; // Namespace Miletos

