#include "packed_file.h"
#include "string_btree.h"
#include "fileid_btree.h"
#include "fileinfo_btree.h"
#include "volume.h"
#include "crypt.h"
#include "hash.h"
#include "compression.h"
#include "file.h"
#include "memory_file.h"
#include "path.h"
#include "util.h"

PackedFile::PackedFile(const char* const file_path, const void* const data, const uint32_t data_size, const uint32_t segment_size)
	: file_path_(nullptr)
	, file_data_(nullptr)
	, segment_size_(0)
	, name_table_offset_(0)
	, extension_table_offset_(0)
	, file_info_table_offset_(0)
	, num_file_id_trees_(0)
{
	const uint32_t magic = data_at<uint32_t>(data, 0x00);
	if (magic == kMAGIC) {
		file_path_ = file_path;
		file_data_ = new char[data_size];
		memcpy(file_data_, data, data_size);
		segment_size_ = segment_size;
		name_table_offset_ = data_at<uint32_t>(file_data_, 0x04);
		extension_table_offset_ = data_at<uint32_t>(file_data_, 0x08);
		file_info_table_offset_ = data_at<uint32_t>(file_data_, 0x0C);
		num_file_id_trees_ = data_at<uint32_t>(file_data_, 0x10);
	}
}

PackedFile::~PackedFile()
{
	delete[] file_data_;
}

bool PackedFile::get_file_info(const char* const file_path, FileInfo* const file_info) const
{
	StringBTree name_btree(advance_pointer(file_data_, name_table_offset_));
	StringBTree extension_btree(advance_pointer(file_data_, extension_table_offset_));

	FileInfoBTree file_info_btree(advance_pointer(file_data_, file_info_table_offset_));
	uint32_t file_info_id = BTreeBase::kINVALID_INDEX;

	const char* part = file_path;
	uint32_t index = 0;
	while (*part != '\0') {
		const char* const delimeter = path_get_delimeter(part);
		const char* extension = nullptr;
		if (delimeter > part) {
			if (*(delimeter - 1) != '.') {
				extension = delimeter - 1;
				do {
					extension--;
				} while (extension > part && *extension != '.');
				if (*extension != '.')
					extension = nullptr;
			}
		} else {
			assert(0);
		}
		const size_t part_length = extension ? (extension - part) : (delimeter - part);

		const uint32_t name_index = name_btree.index_by_string(part, part_length);
		const uint32_t extension_index = extension ? extension_btree.index_by_string(extension) : 0;

		if (name_index != BTreeBase::kINVALID_INDEX) {
			FileIdBTree file_id_btree(advance_pointer(file_data_, file_id_table_offset(index)));
			const void* const file_id_data = file_id_btree.search(name_index, extension_index);
			if (file_id_data) {
				FileId file_id;
				if (file_id.parse(file_id_data)) {
					if ((file_id.flag & FileId::kDIRECTORY_FLAG) != 0)
						index = file_id.entry_index;
					else
						file_info_id = file_id.entry_index;
				} else {
					assert(0);
				}
			}
		}

		part = path_skip_delimeter(delimeter);
	}

	if (file_info_id != BTreeBase::kINVALID_INDEX) {
		const void* const data = file_info_btree.search(file_info_id);
		if (data)
			return file_info->parse(data) != nullptr;
	}

	return false;
}

uint32_t PackedFile::file_id_table_offset(const uint32_t index) const
{
	const uint32_t offset = data_at<uint32_t>(file_data_, kHEADER_SIZE + index * sizeof(uint32_t));
	return offset;
}

uint64_t PackedFile::data_offset() const
{
	const uint64_t offset = align_up(kSEGMENT_SIZE + segment_size_, kSEGMENT_SIZE);
	return offset;
}

struct TraverseEntryArgs
{
	const PackedFile* packed_file;
	const char* output_dir;
	unsigned int depth;
	char prefix[256];
};

bool build_entry_path(const PackedFile& packed_file, const FileId& file_id, const char* const prefix, const char** const path)
{
	StringBTree name_btree(advance_pointer(packed_file.file_data(), packed_file.name_table_offset()));
	StringBTree extension_btree(advance_pointer(packed_file.file_data(), packed_file.extension_table_offset()));

	String name;
	String extension;

	const bool has_name = name_btree.search_by_index(file_id.name_index, &name);
	const bool has_extension = extension_btree.search_by_index(file_id.extension_index, &extension) && extension.length != 0;

	const unsigned int prefix_length = strlen(prefix);

	unsigned int path_length = prefix_length;
	if (has_name) {
		if (has_extension)
			path_length += name.length + extension.length;
		else
			path_length += name.length;
	} else if (has_extension) {
		assert(0);
	}

	char* path_buffer = new char[path_length + 1];
	memset(path_buffer, 0, path_length + 1);
	if (prefix_length != 0)
		memcpy(path_buffer, prefix, prefix_length);
	if (has_name) {
		memcpy(path_buffer + prefix_length, name.data, name.length);
		if (has_extension)
			memcpy(path_buffer + prefix_length + name.length, extension.data, extension.length);
	}
	path_buffer[path_length] = '\0';

	*path = path_buffer;

	return true;
}

int traverse_callback(const FileId* const file_id, void* const arg)
{
	TraverseEntryArgs* const args = static_cast<TraverseEntryArgs*>(arg);

	const bool is_directory = (file_id->flag & FileId::kDIRECTORY_FLAG) != 0;
	const char* path;
	if (!build_entry_path(*args->packed_file, *file_id, args->prefix, &path))
		path = nullptr;

	if (is_directory) {
		assert(path != nullptr);

		TraverseEntryArgs new_args = { args->packed_file, args->output_dir, args->depth + 2 };
		strncpy(new_args.prefix, path, sizeof(new_args.prefix));
		strcat(new_args.prefix, "/");

		FileIdBTree child_id_btree(advance_pointer(args->packed_file->file_data(), args->packed_file->file_id_table_offset(file_id->entry_index)));
		child_id_btree.traverse(&traverse_callback, &new_args);
	} else {
		assert(path != nullptr);

		FileInfoBTree info_btree(advance_pointer(args->packed_file->file_data(), args->packed_file->file_info_table_offset()));
		const void* const data = info_btree.search(file_id->entry_index);
		FileInfo file_info;
		if (file_info.parse(data)) {
			char file_path[_MAX_PATH];
			if (generate_file_path(file_info.entry_index, file_path, sizeof(file_path))) {
				char* data = nullptr;
				uint64_t data_offset = args->packed_file->data_offset() + file_info.segment_index * uint64_t(PackedFile::kSEGMENT_SIZE);
				uint32_t data_size = file_info.size1;

				if (read_file(args->packed_file->file_path(), &data, &data_size, data_offset)) {
					crypt_segment(data, data_size, file_info.entry_index);

					char full_file_path[_MAX_PATH];
					_snprintf(full_file_path, sizeof(full_file_path), "%s%s", args->output_dir, path);

					char directory_path[_MAX_PATH];
					char* p = strrchr(full_file_path, '/');
					if (p) {
						const size_t length = p - full_file_path + 1;
						strncpy(directory_path, full_file_path, length);
						directory_path[length] = '\0';
						make_directories(directory_path);
					}

					if (data_at<uint32_t>(data, 0) == 0xC5EEF7FFu) {
						const uint32_t decompressed_size = -little_to_host(*((int32_t*)data + 1));
						char* out_data = new char[decompressed_size];
						size_t out_size = decompressed_size;
						if (inflate(data + 8, data_size - 8, out_data, &out_size, -15) == 0)
							write_file(full_file_path, out_data, decompressed_size);
						delete[] out_data;
					} else {
						write_file(full_file_path, data, data_size);
					}
				}

				delete[] data;
			}
		}
	}

 	delete[] path;

	return FileIdBTree::kTRAVERSE_CONTINUE;
}

void PackedFile::extract_entries(const char* const output_dir)
{
	TraverseEntryArgs args = { this, output_dir, 0 };
	memset(args.prefix, 0, sizeof(args.prefix));

	FileIdBTree id_btree(advance_pointer(file_data_, file_id_table_offset(0)));
	id_btree.traverse(&traverse_callback, &args);
}
