#include "resource_pack.h"
#include "exception.h"
#ifdef _MSC_VER
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#endif

namespace Halley {
	static const char* IDENTIFIER="HLYRSPCK";

	ResourcePack::ResourcePack(String name)
		: priority(0)
		, fileP(nullptr)
	{
		if (name != "") {
			load(name);
		}
	}

	ResourcePack::~ResourcePack()
	{
		if (fileP) {
			fclose(fileP);
			fileP = nullptr;
		}
	}

	void ResourcePack::add(String diskPath, String file, bool encrypted, bool zipped)
	{
		ResourcePackEntry entry;
		entry.diskPath = diskPath;
		entry.name = file.AsciiLower();
		entry.pos = 0;
		entry.size = 0;
		entry.sizeOnDisk = 0;
		entry.encrypted = encrypted;
		entry.zipped = zipped;
		entries[entry.name] = entry;
	}

	void ResourcePack::save(String path)
	{
		FILE* fp = fopen(path.c_str(), "wb");
		if (!fp) throw Exception("Unable to open destination file");

		// Write file header
		writeHeader(fp);

		// Store TOC start position
		size_t tocStart = ftell(fp);
		writeTOC(fp);

		// Write all files
		for (auto iter = entries.begin(); iter != entries.end(); iter++) {
			writeFile(fp, iter->second);
		}

		// Rewrite TOC, now that we have more data about each file
		fseek(fp, tocStart, SEEK_SET);
		writeTOC(fp);

		// Done
		fclose(fp);
	}

	void ResourcePack::writeHeader(FILE* fp)
	{
		fwrite(IDENTIFIER, 1, 8, fp);

		int version = 1;
		fwrite(&version, 4, 1, fp);
		fwrite(&priority, 4, 1, fp);
	}

	void ResourcePack::writeTOC(FILE* fp)
	{
		// Number of elements
		char buffer[1024];
		int elems = entries.size();
		fwrite(&elems, 4, 1, fp);

		for (auto iter = entries.begin(); iter != entries.end(); iter++) {
			ResourcePackEntry& entry = iter->second;

			// Name
			int len = entry.name.length();
			fwrite(&len, 4, 1, fp);
			strcpy(buffer, entry.name.c_str());
			encrypt(buffer, len);
			fwrite(buffer, 1, len, fp);

			// Pos and sizes
			fwrite(&entry.pos, 4, 1, fp);
			fwrite(&entry.size, 4, 1, fp);
			fwrite(&entry.sizeOnDisk, 4, 1, fp);

			// Flags
			int flags = 0;
			flags |= entry.encrypted ? 1 : 0;
			flags |= entry.zipped ? 2 : 0;
			fwrite(&flags, 4, 1, fp);
		}
	}

	void ResourcePack::writeFile(FILE* fp, ResourcePackEntry& entry)
	{
		std::cout << "Packing " << entry.name;

		try {

			// Current position is where it'll be
			entry.pos = ftell(fp);

			// Open
			FILE* fp2 = fopen(entry.diskPath.c_str(), "rb");
			if (!fp2) throw Exception("Unable to open file: "+entry.diskPath);

			// Figure out file size
			fseek(fp2, 0, SEEK_END);
			size_t len = ftell(fp2);
			fseek(fp2, 0, SEEK_SET);
			entry.size = len;
			entry.sizeOnDisk = len;

			// Read
			char* buf = new char[len];
			if (fread(buf, 1, len, fp2) != len) throw Exception("Unable to read file");
			fclose(fp2);

			// Encrypt
			if (entry.encrypted) encrypt(buf, len);

			// Write
			if (fwrite(buf, 1, len, fp) != len) throw Exception("Unable to write file");

			std::cout << " [OK]" << std::endl;
		} catch (...) {
			std::cout << " [Error]" << std::endl;
			throw;
		}
	}

	void ResourcePack::load(String path)
	{
		name = path;
		FILE* fp = fopen(path.c_str(), "rb");
		if (!fp) throw Exception("Unable to open resource pack: "+path);

		readHeader(fp);
		readTOC(fp);

		fileP = fp;
	}

	void ResourcePack::readHeader(FILE* fp)
	{
		// Read format
		char buf[10];
		fread(buf, 1, 8, fp);
		buf[8] = 0;
		if (strncmp(buf, IDENTIFIER, 8) != 0) throw Exception("Invalid file format");

		// Read version
		int version;
		fread(&version, 4, 1, fp);
		// TODO
		
		// Read priority
		fread(&priority, 4, 1, fp);
	}
	
	void ResourcePack::readTOC(FILE* fp)
	{
		int elems;
		fread(&elems, 4, 1, fp);
		char buffer[1024];

		for (int i=0; i<elems; i++) {
			ResourcePackEntry entry;

			// Read file name
			int len;
			fread(&len, 4, 1, fp);
			if (len >= 1024) throw Exception("Filename too long!");
			fread(buffer, 1, len, fp);
			buffer[len] = 0;
			decrypt(buffer, len);
			entry.name = buffer;

			// Pos and sizes
			fread(&entry.pos, 4, 1, fp);
			fread(&entry.size, 4, 1, fp);
			fread(&entry.sizeOnDisk, 4, 1, fp);

			// Flags
			int flags = 0;
			fread(&flags, 4, 1, fp);
			entry.encrypted = (flags & 1) != 0;
			entry.zipped = (flags & 2) != 0;

			entries[entry.name] = entry;
		}
	}

	shared_ptr<ResourceData> ResourcePack::doGet(String resource)
	{
		auto iter = entries.find(resource);
		if (iter == entries.end()) throw Exception("Resource not found: "+resource);

		ResourcePackEntry& entry = iter->second;
		size_t sz = entry.size;
		char* buf = new char[sz];

		// Read from file
		fseek(fileP, entry.pos, SEEK_SET);
		fread(buf, 1, sz, fileP);

		// Decrypt
		if (entry.encrypted) decrypt(buf, sz);

		return shared_ptr<ResourceData>(new ResourceData(buf, sz, name+"/"+resource));
	}

	std::vector<String> ResourcePack::getResourceList()
	{
		std::vector<String> result;
		for (auto iter = entries.begin(); iter != entries.end(); iter++) {
			result.push_back(iter->first);
		}
		return result;
	}

	void ResourcePack::encrypt(char* data, size_t size)
	{
		char mask = -1;
		char last = 0;
		for (size_t i=0; i<size; i++) {
			data[i] = (data[i] + (last+(i*57))) ^ mask;
			last = data[i];
		}
	}

	void ResourcePack::decrypt(char* data, size_t size)
	{
		char mask = -1;
		char last = 0;
		for (size_t i=0; i<size; i++) {
			char tmp = data[i];
			data[i] = (data[i] ^ mask) - (last+(i*57));
			last = tmp;
		}
	}
}
