#include "objparser.h"

#include <iostream>
#include <string>
#include <sstream>
#include <glm/glm.hpp>

#include "types.h"
#include "resource.h"

namespace kek::ObjParser {

static bool readFaceVertex(std::istringstream &stream, uint32_t *outVertex, uint32_t *outTexture, uint32_t *outNormal) {
	std::string face;
	stream >> face;
	size_t first = face.find('/');
	if(first == std::string::npos) return false;
	size_t second = face.find('/', first + 1);
	if(second == std::string::npos) return false;

	*outVertex = std::stoi(face.substr(0, first));
	*outTexture = std::stoi(face.substr(first + 1, second));
	*outNormal = std::stoi(face.substr(second + 1, face.length()));

	return true;
}

Mesh *parseMesh(MemoryBuffer *buf, std::string folderPath) {
	std::map<std::string, Material *> materials;

	std::vector<glm::vec3> vertexPositions;
	std::vector<glm::vec3> vertexNormals;
	std::vector<glm::vec2> vertexTexCoords;

	std::vector<Vertex> vertices;
	std::vector<uint32_t> indices;

	Material *material = nullptr;

	std::istream stream(buf);

	std::string line;
	while(std::getline(stream, line)) {
		std::istringstream str(line);
		std::string cmd;
		str >> cmd;

		if(cmd == "#") continue;

		if(cmd == "v") {
			float x, y, z;
			str >> x >> y >> z;
			vertexPositions.push_back(glm::vec3(x, y, z));
		}else if(cmd == "vn") {
			float x, y, z;
			str >> x >> y >> z;
			vertexNormals.push_back(glm::normalize(glm::vec3(x, y, z)));
		}else if(cmd == "vt") {
			float u, v = 1.0f;
			str >> u >> v;
			vertexTexCoords.push_back(glm::vec2(u, v));
		}else if(cmd == "f") {
			uint32_t v1, v1T, v1N;
			readFaceVertex(str, &v1, &v1T, &v1N);
			uint32_t v2, v2T, v2N;
			readFaceVertex(str, &v2, &v2T, &v2N);
			uint32_t v3, v3T, v3N;
			readFaceVertex(str, &v3, &v3T, &v3N);

			vertices.push_back(Vertex(vertexPositions[v1 - 1], vertexNormals[v1N - 1], vertexTexCoords[v1T - 1]));
			indices.push_back(indices.size());
			vertices.push_back(Vertex(vertexPositions[v2 - 1], vertexNormals[v2N - 1], vertexTexCoords[v2T - 1]));
			indices.push_back(indices.size());
			vertices.push_back(Vertex(vertexPositions[v3 - 1], vertexNormals[v3N - 1], vertexTexCoords[v3T - 1]));
			indices.push_back(indices.size());
		}else if(cmd == "mtllib") {
			std::string name;
			str >> name;
			std::string mtllibPath = folderPath + name;

			std::map<std::string, Material *> mats = loadMaterials(mtllibPath);
			materials.insert(mats.begin(), mats.end());
		}else if(cmd == "usemtl") {
			std::string mtlName;
			str >> mtlName;
			auto it = materials.find(mtlName);
			if(it == materials.end()) {
				std::cerr << "Invalid material specified: " << mtlName << std::endl;
				throw std::exception();
			}

			material = it->second;
		}else {
			std::cout << "Ignoring unknown/unsupported OBJ command: " << cmd << std::endl;
		}
	}

	for(auto it = materials.begin(); it != materials.end(); it++) {
		if(it->second != material) delete it->second;
	}

	if(!material) {
		std::shared_ptr<Texture> colorTex = Texture::generateColor(glm::vec3(1.0f, 0.0f, 0.0f));
		material = new Material(colorTex, colorTex, colorTex, 1.0f);
	}

	return new Mesh(vertices, indices, material);
}

Mesh *loadMesh(std::string path) {
	std::size_t n = path.find_last_of('/');
	std::string folderPath = n == std::string::npos ? "" : path.substr(0, n + 1);
	MemoryBuffer *buf = Resource::loadResource(path);
	Mesh *mesh = parseMesh(buf, folderPath);
	delete buf;
	return mesh;
}

static Material *makeMaterial(std::shared_ptr<Texture> ambient, std::shared_ptr<Texture> diffuse, std::shared_ptr<Texture> specular, float shininess) {
	if(!ambient) ambient = Texture::generateColor(glm::vec3(1.0f));
	if(!diffuse) diffuse = Texture::generateColor(glm::vec3(1.0f));
	if(!specular) specular = Texture::generateColor(glm::vec3(1.0f));
	return new Material(ambient, diffuse, specular, shininess);
}

std::map<std::string, Material *> parseMaterials(MemoryBuffer *buf, std::string folderPath) {
	std::map<std::string, Material *> mats;

	std::string name;
	std::shared_ptr<Texture> ambient;
	std::shared_ptr<Texture> diffuse;
	std::shared_ptr<Texture> specular;
	float shininess = 0.0f;

	std::istream stream(buf);

	std::string line;
	while(std::getline(stream, line)) {
		std::istringstream str(line);
		std::string cmd;
		str >> cmd;

		if(cmd == "#") continue;

		if(cmd == "newmtl") {
			if(name.length() > 0) mats.emplace(name, makeMaterial(ambient, diffuse, specular, shininess));

			str >> name;
		}else if(cmd == "Ka") {
			float r, g, b;
			str >> r >> g >> b;
			ambient = Texture::generateColor(glm::vec3(r, g, b));
		}else if(cmd == "Kd") {
			float r, g, b;
			str >> r >> g >> b;
			diffuse = Texture::generateColor(glm::vec3(r, g, b));
		}else if(cmd == "Ks") {
			float r, g, b;
			str >> r >> g >> b;
			specular = Texture::generateColor(glm::vec3(r, g, b));
		}else if(cmd == "Ns") {
			float s;
			str >> s;
			shininess = s;
		}else {
			std::cout << "Ignoring unknown/unsupported MTL command: " << cmd << std::endl;
		}
	}

	if(name.length() > 0) mats.emplace(name, makeMaterial(ambient, diffuse, specular, shininess));

	return mats;
}

std::map<std::string, Material *> loadMaterials(std::string path) {
	std::size_t n = path.find_last_of('/');
	std::string folderPath = n == std::string::npos ? "" : path.substr(0, n + 1);
	MemoryBuffer *buf = Resource::loadResource(path);
	std::map<std::string, Material *> mats = parseMaterials(buf, folderPath);
	delete buf;
	return mats;
}

}
