#include <iostream>
#include <vector>
#include <zip.h>
#include <tomlcpp.hpp> //dynamically link tomlcpp if it becomes common in repositories
#include <model.hpp>
#include <config.hpp>

#define BUFFER_SIZE_MODEL_DESC	8192		// 8 KiB
#define BUFFER_SIZE_TEXTURE	16777220	// 16 MiB

#define SUPPORTED_MODEL_MAJOR 0
#define SUPPORTED_MODEL_MINOR 1

void textureFromArchive(zip_t* archive, const char* path, ModelPart* part, size_t slot, std::string triggerName) {
	zip_file_t* textureFile = zip_fopen(archive, path, 0);
	if (textureFile != NULL) {
		unsigned char* textureBuffer = new unsigned char[BUFFER_SIZE_TEXTURE];
		size_t textureLength = zip_fread(textureFile, textureBuffer, BUFFER_SIZE_TEXTURE-1);

		part->addTexture(textureBuffer, textureLength, slot, triggerName);

		delete [] textureBuffer;
	} else {
		std::cerr << path << " does not exist in archive!" << std::endl;
	}
}

Model::Model(const char* path) {
	int zipError;
	zip_t* archive = zip_open(path, ZIP_RDONLY, &zipError);

	if (!archive) {
		std::cerr << "Model file " << path << " does not exist!" << std::endl;
		return;
	}

	// get model description file (model.toml)
	zip_file_t* modelDescFile = zip_fopen(archive, "model.toml", 0);
	char modelDescBuffer[BUFFER_SIZE_MODEL_DESC];
	size_t descLength = zip_fread(modelDescFile, modelDescBuffer, BUFFER_SIZE_MODEL_DESC-1);
	modelDescBuffer[descLength] = 0;	//null-terminate

	// parse model.toml
	auto modelDesc = toml::parse(std::string(modelDescBuffer));
	if (!modelDesc.table) {
		std::cerr << "cannot parse model.toml! " << std::endl << modelDesc.errmsg << std::endl;
	}

	// get format table
	auto format = modelDesc.table->getTable("format");
	if (!format) {
		std::cerr << "Model does not have a format table!" << std::endl;
	} else {
		// get format version
		auto formatMajResult = format->getInt("version_major");
		auto formatMinResult = format->getInt("version_minor");

		if (formatMajResult.first && formatMinResult.first) {
			// check format version
			if (formatMajResult.second != SUPPORTED_MODEL_MAJOR ||
				formatMinResult.second > SUPPORTED_MODEL_MINOR ) {

				std::cerr << "Model format version " <<
					formatMajResult.second << "." << formatMinResult.second <<
					" is unsupported! This version of " << PROJECT_NAME << " supports model file version " <<
					SUPPORTED_MODEL_MAJOR << ".0 to version " <<
					SUPPORTED_MODEL_MAJOR << "." << SUPPORTED_MODEL_MINOR << std::endl;
			}
		} else {
			std::cerr << "Model does not define a format version!" << std::endl;
		}
	}


	// get model info table
	auto modelInfo = modelDesc.table->getTable("model_info");

	if (!modelInfo) {
		std::cerr << "Model does not have a model_info table!" << std::endl;
	} else {

		// get name
		auto nameResult = modelInfo->getString("name");

		if (!nameResult.first) {
			std::cerr << "Model does not have a name!" << std::endl;
		} else {
			name = nameResult.second;
		}
	}

	// parse parts
	auto partsDescArray = modelDesc.table->getArray("part");

	auto partsVec = *partsDescArray->getTableVector();

	for (int i = 0; i < partsVec.size(); i++) {
		ModelPart newPart;

		// position
		auto bindResult = partsVec[i].getString("bind");
		if (!bindResult.first) {
			std::cerr << "Part " << i << " does not define a bind!" << std::endl;
		} else {
			newPart.setBind(bindResult.second);
		}

		auto parentResult = partsVec[i].getString("follow");
		auto factorResult = partsVec[i].getDouble("factor");

		if (parentResult.first) {
			newPart.setFollowTarget(parentResult.second);

			if (factorResult.first) {
				newPart.setFollowFactor((float)factorResult.second);
			} else {
				newPart.setFollowFactor(1.0f);
			}
		}

		// texture
		auto textureSingle = partsVec[i].getString("texture");

		if (textureSingle.first) {
			// only a single texture was defined
			textureFromArchive(archive, textureSingle.second.c_str(), &newPart, 0, "null");
		} else {
			auto textureArray = partsVec[i].getArray("textures");
			auto textureVec = *textureArray->getTableVector().get();

			if (textureVec.size() < 1) {
				std::cerr << "Part " << i << " does not define any textures!" << std::endl;
			} else {
				// a list of textures with triggers were defined
				for (int j = 0; j < textureVec.size(); j++) {
					auto fileResult = textureVec[j].getString("file");
					auto triggerResult = textureVec[j].getString("trigger");

					if (fileResult.first) {
						std::string trigger = triggerResult.first ? triggerResult.second : "null";
						textureFromArchive(archive, fileResult.second.c_str(), &newPart, j, trigger);
					}
				}
			}
		}
			parts.push_back(newPart);
	}
}

void Model::draw() {
	for (size_t i = 0; i < parts.size(); i++) {
		parts[i].bindAndDraw();
	}
}

void Model::updateTransforms(struct FaceData faceData) {
	for (size_t i = 0; i < parts.size(); i++) {
		parts[i].processFaceData(faceData);
	}
}

std::string Model::getName() {
	return name;
}