#include "shader.h"
#include "types.h"
#include "resource.h"
#include "constants.h"

#include <GL/glew.h>

namespace kek {

static GLuint compileShader(GLenum type, std::string path) {
	MemoryBuffer *buf = Resource::loadResource(path);
	if(!buf) {
		std::cout << "Couldn't load shader: " << path << std::endl;
		throw std::exception();
	}

	std::istream inStream(buf);
	std::ostringstream stream;
	std::string line;
	while(std::getline(inStream, line)) {
		if(line.find("!include ") == 0) {
			std::string includePath = line.substr(9);
			MemoryBuffer *includeBuf = Resource::loadResource("shader/include/" + includePath);
			if(!includeBuf) {
				std::cerr << "Include error: Failed to find path '" << includePath << "'" << std::endl;
				throw std::exception();
			}
			stream << includeBuf;
			delete includeBuf;
		}else {
			stream << line << '\n';
		}
	}

	std::string code = stream.str();
	delete buf;

	const char *src = code.c_str();

	GLint success;

	GLuint shaderID = glCreateShader(type);
	glShaderSource(shaderID, 1, &src, nullptr);
	glCompileShader(shaderID);

	glGetShaderiv(shaderID, GL_COMPILE_STATUS, &success);
	if(!success) {
		char log[512];
		glGetShaderInfoLog(shaderID, 512, nullptr, log);
		std::cout << "Failed to compile shader \"" << path << "\":\n" << log << std::endl;
		throw std::exception();
	}

	return shaderID;
}

static GLuint compileProgram(unsigned int n, GLenum *types, std::string *paths) {
	// Compile shaders
	GLuint shaderIds[n];
	for(unsigned int i = 0; i < n; i++) {
		shaderIds[i] = compileShader(types[i], paths[i]);
	}

	GLuint id = glCreateProgram();

	// Attach shaders
	for(unsigned int i = 0; i < n; i++) {
		glAttachShader(id, shaderIds[i]);
	}

	glLinkProgram(id);

	GLint success;
	glGetProgramiv(id, GL_LINK_STATUS, &success);

	if(!success) {
		char log[512];
		glGetProgramInfoLog(id, 512, nullptr, log);
		std::cout << "Failed to link program:\n" << log << std::endl;
		throw std::exception();
	}

	// Delete shaders
	for(unsigned int i = 0; i < n; i++) {
		glDeleteShader(shaderIds[i]);
	}

	return id;
}

Shader::Shader(std::string vertexPath, std::string fragmentPath) {
	GLenum types[2];
	types[0] = GL_VERTEX_SHADER;
	types[1] = GL_FRAGMENT_SHADER;

	std::string paths[2];
	paths[0] = vertexPath;
	paths[1] = fragmentPath;

	id = compileProgram(2, types, paths);
}

Shader::Shader(std::string vertexPath, std::string geometryPath, std::string fragmentPath) {
	GLenum types[3];
	types[0] = GL_VERTEX_SHADER;
	types[1] = GL_GEOMETRY_SHADER;
	types[2] = GL_FRAGMENT_SHADER;

	std::string paths[3];
	paths[0] = vertexPath;
	paths[1] = geometryPath;
	paths[2] = fragmentPath;

	id = compileProgram(3, types, paths);
}

Shader::~Shader() {
	glDeleteProgram(id);
}

void Shader::initLighting() {
	lighting = (LightingData *) calloc(1, sizeof(LightingData));
	lighting->blockID = glGetUniformBlockIndex(id, "LightInfo");
	glUniformBlockBinding(id, lighting->blockID, KEK_UNIFORM_LIGHTS_BINDING);
	glCreateBuffers(1, &lighting->ubo);

	GLenum err = glGetError();
	if(err != GL_NO_ERROR) {
		std::cout << "OpenGL error while init lighting: " << err << std::endl;
		exit(1);
	}

	glGetActiveUniformBlockiv(id, lighting->blockID, GL_UNIFORM_BLOCK_DATA_SIZE, &lighting->blockSize);
	std::cout << "Lighting block size: " << lighting->blockSize << std::endl;
	std::cout << "Light size: " << sizeof(ShaderLight) << std::endl;
	if(lighting->blockSize != KEK_LIGHT_LIMIT * sizeof(ShaderLight)) {
		std::cerr << "Lighting block size doesn't match (is " << lighting->blockSize << ", should be " << KEK_LIGHT_LIMIT * sizeof(ShaderLight) << "). Improper padding?" << std::endl;
		exit(1);
	}
}

void Shader::use() {
	glUseProgram(id);
}

}
