#include <iostream>
#include <vector>
#include <zip.h>
#include <fmt/core.h>
#include <glm/vec2.hpp>
#include <tomlcpp.hpp> //dynamically link tomlcpp if it becomes common in repositories
#include <model.hpp>
#include <config.hpp>
#include <error.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 3

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 {
		showError(fmt::format("Texture file \"{}\" does not exist in archive!", path), "Could not open model");
	}
}

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

	if (!archive) {
		showError(fmt::format("Model file {} does not exist or is corrupt!", path), "Could not open model");
		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) {
		showError("Cannot parse model.toml:\n" + modelDesc.errmsg, "Could not open model");
	}

	// get format table
	auto format = modelDesc.table->getTable("format");
	if (!format) {
		showError("Model does not have a format table!", "Could not open model");
	} 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 ) {

				showError(fmt::format("Model format version {0}.{1} is unsupported! This version of {2} supports model file version"
						" {3}.0 to version {3}.{4}.",
							formatMajResult.second, formatMinResult.second,
							PROJECT_NAME,
							SUPPORTED_MODEL_MAJOR, SUPPORTED_MODEL_MINOR), "Could not open model");
			}
		} else {
			showError("Model does not define a format version!", "Could not open model");
		}
	}


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

	if (!modelInfo) {
		showError("Model does not have a model_info table!", "Could not open model");
	} else {

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

		if (!nameResult.first) {
			showWarning("Model does not have a name!", "Model warning");
		} 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) {
			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.factor = (float)factorResult.second;
			} else {
				newPart.factor = 1.0f;
			}
		}

		// rotation and scale factor
		auto rotFacResult = partsVec[i].getDouble("rot_factor");
		auto scaleFacResult = partsVec[i].getDouble("scale_factor");
		auto offsetFacResult = partsVec[i].getDouble("offset_factor");

		if (rotFacResult.first) {
			newPart.rotFactor = (float)rotFacResult.second;
		}
		if (scaleFacResult.first) {
			newPart.scaleFactor = (float)scaleFacResult.second;
		}
		if (offsetFacResult.first) {
			newPart.offsetFactor = (float)offsetFacResult.second;
		}
		

		// origin
		auto originArray = partsVec[i].getArray("origin");

		if (originArray) {
			auto originVec = *originArray->getDoubleVector();

			newPart.origin = glm::vec2((float)originVec[0], (float)originVec[1]);
		}

		// offsets
		auto posOffsetArray = partsVec[i].getArray("pos_offset");
		if (posOffsetArray) {
			auto offsetVec = *posOffsetArray->getDoubleVector();

			newPart.posOffset = glm::vec2((float)offsetVec[0], (float)offsetVec[1]);
		}

		auto scaleOffsetArray = partsVec[i].getArray("scale_offset");
		if (scaleOffsetArray) {
			auto offsetVec = *scaleOffsetArray->getDoubleVector();

			newPart.scaleOffset = glm::vec2((float)offsetVec[0], (float)offsetVec[1]);
		}

		// offset bind
		auto offsetBindResult = partsVec[i].getString("offset_bind");
		if (offsetBindResult.first) {
			newPart.setOffsetBind(offsetBindResult.second);
		}


		// 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) {
				showWarning(fmt::format("Part {} does not define any textures! Parts with no textures defined will"
							" show a default \"missing texture\" pattern.", i), "Model warning");
			} 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;
}

