/**
 * Tibia GIMUD Server - a free and open-source MMORPG server emulator
 * Copyright (C) 2019 Sabrehaven and Mark Samman <mark.samman@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "otpch.h"

#include "fileloader.h"

FileLoader::~FileLoader()
{
	if (file) {
		fclose(file);
		file = nullptr;
	}

	NodeStruct::clearNet(root);
	delete[] buffer;

	for (auto& i : cached_data) {
		delete[] i.data;
	}
}

bool FileLoader::openFile(const char* filename, const char* accept_identifier)
{
	file = fopen(filename, "rb");
	if (!file) {
		lastError = ERROR_CAN_NOT_OPEN;
		return false;
	}

	char identifier[4];
	if (fread(identifier, 1, 4, file) < 4) {
		fclose(file);
		file = nullptr;
		lastError = ERROR_EOF;
		return false;
	}

	// The first four bytes must either match the accept identifier or be 0x00000000 (wildcard)
	if (memcmp(identifier, accept_identifier, 4) != 0 && memcmp(identifier, "\0\0\0\0", 4) != 0) {
		fclose(file);
		file = nullptr;
		lastError = ERROR_INVALID_FILE_VERSION;
		return false;
	}

	fseek(file, 0, SEEK_END);
	int32_t file_size = ftell(file);
	cache_size = std::min<uint32_t>(32768, std::max<uint32_t>(file_size / 20, 8192)) & ~0x1FFF;

	if (!safeSeek(4)) {
		lastError = ERROR_INVALID_FORMAT;
		return false;
	}

	delete root;
	root = new NodeStruct();
	root->start = 4;

	int32_t byte;
	if (safeSeek(4) && readByte(byte) && byte == NODE_START) {
		return parseNode(root);
	}

	return false;
}

bool FileLoader::parseNode(NODE node)
{
	int32_t byte, pos;
	NODE currentNode = node;

	while (readByte(byte)) {
		currentNode->type = byte;
		bool setPropsSize = false;

		while (true) {
			if (!readByte(byte)) {
				return false;
			}

			bool skipNode = false;

			switch (byte) {
				case NODE_START: {
					//child node start
					if (!safeTell(pos)) {
						return false;
					}

					NODE childNode = new NodeStruct();
					childNode->start = pos;
					currentNode->propsSize = pos - currentNode->start - 2;
					currentNode->child = childNode;

					setPropsSize = true;

					if (!parseNode(childNode)) {
						return false;
					}

					break;
				}

				case NODE_END: {
					//current node end
					if (!setPropsSize) {
						if (!safeTell(pos)) {
							return false;
						}

						currentNode->propsSize = pos - currentNode->start - 2;
					}

					if (!readByte(byte)) {
						return true;
					}

					switch (byte) {
						case NODE_START: {
							//starts next node
							if (!safeTell(pos)) {
								return false;
							}

							skipNode = true;
							NODE nextNode = new NodeStruct();
							nextNode->start = pos;
							currentNode->next = nextNode;
							currentNode = nextNode;
							break;
						}

						case NODE_END:
							return safeTell(pos) && safeSeek(pos);

						default:
							lastError = ERROR_INVALID_FORMAT;
							return false;
					}

					break;
				}

				case ESCAPE_CHAR: {
					if (!readByte(byte)) {
						return false;
					}

					break;
				}

				default:
					break;
			}

			if (skipNode) {
				break;
			}
		}
	}
	return false;
}

const uint8_t* FileLoader::getProps(const NODE node, size_t& size)
{
	if (!node) {
		return nullptr;
	}

	if (node->propsSize >= buffer_size) {
		delete[] buffer;

		while (node->propsSize >= buffer_size) {
			buffer_size *= 2;
		}

		buffer = new uint8_t[buffer_size];
	}

	//get buffer
	if (!readBytes(node->propsSize, node->start + 2)) {
		return nullptr;
	}

	//unscape buffer
	size_t j = 0;
	bool escaped = false;
	for (uint32_t i = 0; i < node->propsSize; ++i, ++j) {
		if (buffer[i] == ESCAPE_CHAR) {
			//escape char found, skip it and write next
			buffer[j] = buffer[++i];
			//is neede a displacement for next bytes
			escaped = true;
		} else if (escaped) {
			//perform that displacement
			buffer[j] = buffer[i];
		}
	}

	size = j;
	return buffer;
}

bool FileLoader::getProps(const NODE node, PropStream& props)
{
	size_t size;
	if (const uint8_t* a = getProps(node, size)) {
		props.init(reinterpret_cast<const char*>(a), size); // does not break strict aliasing
		return true;
	}

	props.init(nullptr, 0);
	return false;
}

NODE FileLoader::getChildNode(const NODE parent, uint32_t& type)
{
	if (parent) {
		NODE child = parent->child;
		if (child) {
			type = child->type;
		}

		return child;
	}

	type = root->type;
	return root;
}

NODE FileLoader::getNextNode(const NODE prev, uint32_t& type)
{
	if (!prev) {
		return NO_NODE;
	}

	NODE next = prev->next;
	if (next) {
		type = next->type;
	}
	return next;
}

inline bool FileLoader::readByte(int32_t& value)
{
	if (cache_index == NO_VALID_CACHE) {
		lastError = ERROR_CACHE_ERROR;
		return false;
	}

	if (cache_offset >= cached_data[cache_index].size) {
		int32_t pos = cache_offset + cached_data[cache_index].base;
		int32_t tmp = getCacheBlock(pos);
		if (tmp < 0) {
			return false;
		}

		cache_index = tmp;
		cache_offset = pos - cached_data[cache_index].base;
		if (cache_offset >= cached_data[cache_index].size) {
			return false;
		}
	}

	value = cached_data[cache_index].data[cache_offset++];
	return true;
}

inline bool FileLoader::readBytes(uint32_t size, int32_t pos)
{
	//seek at pos
	uint32_t remain = size;
	uint8_t* buf = this->buffer;
	do {
		//prepare cache
		uint32_t i = getCacheBlock(pos);
		if (i == NO_VALID_CACHE) {
			return false;
		}

		cache_index = i;
		cache_offset = pos - cached_data[i].base;

		//get maximum read block size and calculate remaining bytes
		uint32_t reading = std::min<int32_t>(remain, cached_data[i].size - cache_offset);
		remain -= reading;

		//read it
		memcpy(buf, cached_data[cache_index].data + cache_offset, reading);

		//update variables
		cache_offset += reading;
		buf += reading;
		pos += reading;
	} while (remain > 0);
	return true;
}

inline bool FileLoader::safeSeek(uint32_t pos)
{
	uint32_t i = getCacheBlock(pos);
	if (i == NO_VALID_CACHE) {
		return false;
	}

	cache_index = i;
	cache_offset = pos - cached_data[i].base;
	return true;
}

inline bool FileLoader::safeTell(int32_t& pos)
{
	if (cache_index == NO_VALID_CACHE) {
		lastError = ERROR_CACHE_ERROR;
		return false;
	}

	pos = cached_data[cache_index].base + cache_offset - 1;
	return true;
}

inline uint32_t FileLoader::getCacheBlock(uint32_t pos)
{
	bool found = false;
	uint32_t i, base_pos = pos & ~(cache_size - 1);

	for (i = 0; i < CACHE_BLOCKS; i++) {
		if (cached_data[i].loaded) {
			if (cached_data[i].base == base_pos) {
				found = true;
				break;
			}
		}
	}

	if (!found) {
		i = loadCacheBlock(pos);
	}

	return i;
}

int32_t FileLoader::loadCacheBlock(uint32_t pos)
{
	int32_t i, loading_cache = -1, base_pos = pos & ~(cache_size - 1);

	for (i = 0; i < CACHE_BLOCKS; i++) {
		if (!cached_data[i].loaded) {
			loading_cache = i;
			break;
		}
	}

	if (loading_cache == -1) {
		for (i = 0; i < CACHE_BLOCKS; i++) {
			if (std::abs(static_cast<long>(cached_data[i].base) - base_pos) > static_cast<long>(2 * cache_size)) {
				loading_cache = i;
				break;
			}
		}

		if (loading_cache == -1) {
			loading_cache = 0;
		}
	}

	if (cached_data[loading_cache].data == nullptr) {
		cached_data[loading_cache].data = new uint8_t[cache_size];
	}

	cached_data[loading_cache].base = base_pos;

	if (fseek(file, cached_data[loading_cache].base, SEEK_SET) != 0) {
		lastError = ERROR_SEEK_ERROR;
		return -1;
	}

	uint32_t size = fread(cached_data[loading_cache].data, 1, cache_size, file);
	cached_data[loading_cache].size = size;

	if (size < (pos - cached_data[loading_cache].base)) {
		lastError = ERROR_SEEK_ERROR;
		return -1;
	}

	cached_data[loading_cache].loaded = 1;
	return loading_cache;
}
