/*
 * ttm_pack: A packer for RPG Maker XP datafiles.
 *
 * Copyright (c) 2012, David Gow <david@ingeniumdigital.com>
 * Copyright (c) 2013, Jonas Kulla <Nyocurio@gmail.com>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/*
 * To compile: 	cc -o ttm_pack ttm_pack.c
 *
 * To run:	cd <path to your RMXP game>
 * 		ttm_pack [<optional folders / files>] [-o <archive filename>]
 *
 * If no folders / files are manually specified,
 * ttm_pack will automatically attempt to archive
 * the "Graphics" and "Data" folder.
 * If not otherwise specified via "-o", creates
 * the file "Game.rgssad" (careful to not
 * overwrite an existing archive!)
 * If the output filename has the extension ".rgss3a",
 * RGSSAD format version 3 (RGP Maker VX Ace)
 * will be used instead of 1 (XP / VX).
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>

void advanceFileKey(uint32_t *key)
{
	*key = *key * 7 + 3;
}

void writeHeader(FILE *arch, char version)
{
	fwrite("RGSSAD", 1, 7, arch);
	fwrite(&version, 1, 1, arch);
}

struct WriteContext;
typedef void (*EntryProc)(struct WriteContext *ctx, const char *path);

struct WriteContext
{
	EntryProc fileProc;
	EntryProc symProc;
	void *data;

	void *writeBuf;
	size_t writeBufSize;

	size_t filesWritten;
};

void handleEntry(struct WriteContext *ctx, const char *entry);

void handleDir(struct WriteContext *ctx, const char *path)
{
	DIR *dir;
	struct dirent *ent;
	char buffer[512];

	dir = opendir(path);
	if (!dir)
	{
		printf("Couldn't write directory: %s\n", path);
		return;
	}

	while ((ent = readdir(dir)) != 0)
	{
		if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, ".."))
			continue;

		snprintf(buffer, sizeof(buffer), "%s/%s", path, ent->d_name);
		handleEntry(ctx, buffer);
	}

	closedir(dir);
}

void writeFileBlob(FILE *arch, FILE *file, size_t fileSize,
                   uint32_t *key, struct WriteContext *ctx)
{
	size_t rem = fileSize;
	size_t i;

	while (rem > 0)
	{
		size_t n = fread(ctx->writeBuf, 1, ctx->writeBufSize, file);
		rem -= n;

		size_t dwords = (n / sizeof(uint32_t)) + ((n % sizeof(uint32_t) == 0) ? 0 : 1);

		for (i = 0; i < dwords; ++i)
		{
			((uint32_t*) ctx->writeBuf)[i] ^= *key;
			advanceFileKey(key);
		}

		fwrite(ctx->writeBuf, 1, n, arch);
	}
}

void handleEntry(struct WriteContext *ctx, const char *path)
{
	struct stat st;
	stat(path, &st);

	if (S_ISDIR(st.st_mode))
	{
		handleDir(ctx, path);
		return;
	}

	if (S_ISREG(st.st_mode))
	{
		ctx->fileProc(ctx, path);
		return;
	}

	if (S_ISLNK(st.st_mode))
	{
		ctx->symProc(ctx, path);
		return;
	}

	printf("Couldn't write entry: %s\n", path);
}

struct Array
{
	char *base;
	size_t capa;
	size_t used;
};

typedef size_t array_ind_t;

void array_init(struct Array *sa)
{
	sa->used = 0;
	sa->capa = 0x4000;
	sa->base = malloc(sa->capa);
}

void array_fini(struct Array *sa)
{
	free(sa->base);
}

/* Returns byte offset into array store */
array_ind_t array_append(struct Array *sa, const void *data, size_t len)
{
	size_t oldUsed = sa->used;
	sa->used += len;

	while (sa->used > sa->capa)
	{
		sa->capa *= 1.5;
		sa->base = realloc(sa->base, sa->capa);
	}

	char *slice = &sa->base[oldUsed];

	if (data)
		memcpy(slice, data, len+1);

	return oldUsed;
}

void *array_get_data(struct Array *sa, array_ind_t ind)
{
	return &sa->base[ind];
}

struct ArgData
{
	int argc;
	char **argv;
	int outI;
	FILE *arch;
};

void processArguments(struct WriteContext *ctx, struct ArgData *arg)
{
	if ((arg->outI < 1 && arg->argc < 2) || (arg->outI >= 1 && arg->argc < 4))
	{
		handleDir(ctx, "Graphics");
		handleDir(ctx, "Data");

		return;
	}

	int i;
	for (i = 1; i < arg->argc; ++i)
	{
		if (i == arg->outI)
		{
			++i;
			continue;
		}

		char *ent = arg->argv[i];
		char *e = &ent[strlen(ent)];

		if (e != ent && *(e-1) == '/')
			*(e-1) = '\0';

		handleEntry(ctx, ent);
	}
}

struct Context1
{
	FILE *arch;
	uint32_t key;
};

void handleFile1(struct WriteContext *ctx, const char *path)
{
	printf("Writing file: %s\n", path);

	struct Context1 *ctx1 = ctx->data;

	FILE *arch = ctx1->arch;
	FILE *file = fopen(path, "r");

	if (!file)
	{
		printf("  ..failed\n");
		return;
	}

	uint32_t pathLen = strlen(path);
	uint32_t pathLenEnc = pathLen ^ ctx1->key;
	advanceFileKey(&ctx1->key);

	fwrite(&pathLenEnc, sizeof(pathLenEnc), 1, arch);

	char buffer[512];
	strncpy(buffer, path, sizeof(buffer));

	int i;
	for (i = 0; i < pathLen; ++i)
	{
		if (buffer[i] == '/')
			buffer[i] = '\\'; /* puke */

		buffer[i] ^= (ctx1->key & 0xFF);
		advanceFileKey(&ctx1->key);
	}

	fwrite(buffer, 1, pathLen, arch);

	fseek(file, 0, SEEK_END);
	uint32_t fileSize = ftell(file);
	fseek(file, 0, SEEK_SET);
	uint32_t fileSizeEnc = fileSize ^ ctx1->key;
	advanceFileKey(&ctx1->key);

	fwrite(&fileSizeEnc, sizeof(fileSizeEnc), 1, arch);

	uint32_t oldState = ctx1->key;

	writeFileBlob(arch, file, fileSize, &ctx1->key, ctx);
	fclose(file);

	ctx1->key = oldState;

	++ctx->filesWritten;
}

void handleSym1(struct WriteContext *ctx, const char *path)
{
	printf("Symlink stub: %s\n", path);
}

void writeArch1(struct WriteContext *ctx, struct ArgData *arg)
{
	struct Context1 ctx1;
	ctx1.arch = arg->arch;
	ctx1.key = 0xDEADCAFE;

	ctx->fileProc = handleFile1;
	ctx->symProc = handleSym1;
	ctx->data = &ctx1;

	processArguments(ctx, arg);
}

struct FileInfo
{
	array_ind_t path;
	size_t pathLen;
};

struct FileInfo *file_info_array_get(struct Array *a, size_t i)
{
	return (struct FileInfo*) &a->base[sizeof(struct FileInfo) * i];
}

struct Context3
{
	size_t fileInfoN;
	struct Array fileInfos;
	struct Array filePaths;

	size_t tableSize;
};

void handleFile3(struct WriteContext *ctx, const char *path)
{
	struct Context3 *ctx3 = ctx->data;

	++ctx3->fileInfoN;
	size_t infoOff = array_append(&ctx3->fileInfos, 0, sizeof(struct FileInfo));
	struct FileInfo *info = (struct FileInfo*) &ctx3->fileInfos.base[infoOff];

	size_t pathLen = strlen(path);
	info->path = array_append(&ctx3->filePaths, path, pathLen+1);
	info->pathLen = pathLen;

	ctx3->tableSize += sizeof(uint32_t) * 4 + pathLen;
}

size_t writeDWord3(FILE *f, uint32_t dword, const uint32_t key)
{
	uint32_t val = dword ^ key;
	return fwrite(&val, sizeof(val), 1, f);
}

void writeArch3(struct WriteContext *ctx, struct ArgData *arg)
{
	const uint32_t wBaseKey = 0;
	const uint32_t baseKey = (wBaseKey * 9) + 3;
	fwrite(&wBaseKey, sizeof(wBaseKey), 1, arg->arch);

	struct Context3 ctx3;
	array_init(&ctx3.fileInfos);
	array_init(&ctx3.filePaths);
	ctx3.fileInfoN = 0;
	/* Reserve space for last null entry */
	ctx3.tableSize = sizeof(uint32_t) * 4;

	ctx->fileProc = handleFile3;
	ctx->symProc = handleSym1;
	ctx->data = &ctx3;

	processArguments(ctx, arg);

	size_t fileTableOff = ftell(arg->arch);
	size_t dataOff = fileTableOff + ctx3.tableSize;

	printf("Table offset: %X, data offset: %X\n", fileTableOff, dataOff);

	size_t i;
	for (i = 0; i < ctx3.fileInfoN; ++i)
	{
		struct FileInfo *info = file_info_array_get(&ctx3.fileInfos, i);
		const char *path = array_get_data(&ctx3.filePaths, info->path);
		printf("Writing file: %s\n", path);

		char buf[1024];
		if (info->pathLen > sizeof(buf))
		{
			printf("Filename too long: %s\n", info->pathLen);
			continue;
		}

		FILE *file = fopen(path, "r");

		if (!file)
			continue;

		uint32_t fileKey = 0;

		size_t size;
		fseek(file, 0, SEEK_END);
		size = ftell(file);
		fseek(file, 0, SEEK_SET);

		/* Write file table entry */
		fseek(arg->arch, fileTableOff, SEEK_SET);
		writeDWord3(arg->arch, dataOff, baseKey);
		writeDWord3(arg->arch, size, baseKey);
		writeDWord3(arg->arch, fileKey, baseKey);
		writeDWord3(arg->arch, info->pathLen, baseKey);

		/* Write file path */
		uint32_t j;
		for (j = 0; j < info->pathLen; ++j)
		{
			char c = path[j];

			if (c == '/')
				c = '\\';

			c ^= ((baseKey >> 8*(j % 4)) & 0xFF);

			buf[j] = c;
		}

		fwrite(buf, info->pathLen, 1, arg->arch);

		fileTableOff = ftell(arg->arch);

		/* Write data */
		fseek(arg->arch, dataOff, SEEK_SET);
		writeFileBlob(arg->arch, file, size, &fileKey, ctx);

		fclose(file);

		dataOff = ftell(arg->arch);

		++ctx->filesWritten;
	}

	/* Write delimiting null entry */
	fseek(arg->arch, fileTableOff, SEEK_SET);
	writeDWord3(arg->arch, 0, baseKey);

	array_fini(&ctx3.fileInfos);
	array_fini(&ctx3.filePaths);
}

int main(int argc, char *argv[])
{
	const char *archName = "Game.rgssad";

	int i;
	int outArgI = -1;

	for (i = 1; i < argc; ++i)
		if (strcmp(argv[i], "-o") == 0 && i+1 < argc)
		{
			archName = argv[i+1];
			outArgI = i;
		}

	const char *ext;
	for (ext = &archName[strlen(archName)]; ext != archName; --ext)
		if (*ext == '.')
			break;

	char ver = 1;
	if (strcmp(ext, ".rgss3a") == 0)
		ver = 3;

	FILE *arch = fopen(archName, "wb");

	if (!arch)
	{
		printf("Error: could not open archive \"%s\" for writing.", archName);
		return -1;
	}

	writeHeader(arch, ver);

	struct WriteContext ctx;
	ctx.writeBufSize = 0x1000;
	ctx.writeBuf = malloc(ctx.writeBufSize);
	ctx.filesWritten = 0;

	struct ArgData arg;
	arg.argc = argc;
	arg.argv = argv;
	arg.outI = outArgI;
	arg.arch = arch;

	if (ver == 1)
		writeArch1(&ctx, &arg);
	else
		writeArch3(&ctx, &arg);

	fseek(arch, 0, SEEK_END);
	printf("%u files packed, %u bytes written\n", ctx.filesWritten, ftell(arch));

	free(ctx.writeBuf);

	fclose(arch);

	return 0;
}
 
