
#include <Spectre/System/Log.h>
#include <Spectre/Graphics/OpenGL.h>
#include <Spectre/Graphics/Renderable.h>
#include <Spectre/Graphics/RenderState.h>
#include <Spectre/Graphics/ShaderProgram.h>
#include <Spectre/Graphics/Texture.h>
#include <Spectre/Graphics/BatchRenderer2D.h>
#include <Spectre/Math/Math.h>

#include <algorithm>

static bool compare_renderable(const Renderable2D* a, const Renderable2D* b) {

	if (a->getZOrder() != b->getZOrder()) {
		return a->getZOrder() < b->getZOrder();
	}
	if (a->getRenderType() != b->getRenderType()) {
		return a->getRenderType() < b->getRenderType();
	}
	if (a->getTexture() != b->getTexture()) {
		return a->getTexture() < b->getTexture();
	}
	return false;
}

BatchRenderer2D::BatchRenderer2D()
{
	// Generate buffer objects.
	glGenBuffers(1, &m_IBO);
	glGenBuffers(1, &m_VBO);

	// Generate and bind Vertex Array Object
	glGenVertexArrays(1, &m_VAO);
	glBindVertexArray(m_VAO);

	setBatchSize(10000);

	// Setup position.
	glEnableVertexAttribArray(VertexAttribPosition);
	glVertexAttribPointer(VertexAttribPosition, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), (void*)0);

	// Setup color
	glEnableVertexAttribArray(VertexAttribColor0);
	glVertexAttribPointer(VertexAttribColor0, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), (void*)8);

	glEnableVertexAttribArray(VertexAttribTexCoord0);
	glVertexAttribPointer(VertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), (void*)24);

	m_textShader.create();
	if (!m_textShader.loadFromFile("assets/shaders/text.shader.glsl")) {
		std::cout << "Failed to load shader: " << m_textShader.getLastError() << std::endl;
	}

	if (!m_textShader.link()) {
		std::cout << "Failed to link shader: " << m_textShader.getLastError() << std::endl;
	}

	m_spriteShader.create();
	if (!m_spriteShader.loadFromFile("assets/shaders/standard.shader.glsl")) {
		std::cout << "Failed to load shader: " << m_spriteShader.getLastError() << std::endl;
	}

	if (!m_spriteShader.link()) {
		std::cout << "Failed to link shader: " << m_spriteShader.getLastError() << std::endl;
	}
}

BatchRenderer2D::~BatchRenderer2D()
{
	glDeleteBuffers(1, &m_VBO);
	glDeleteBuffers(1, &m_IBO);
	glDeleteVertexArrays(1, &m_VAO);
}

void BatchRenderer2D::setBatchSize(unsigned short size)
{
	std::vector<unsigned short> indices;
	int vtx = 0;

	if (size > 10000) {
		size = 10000;
	}
	m_size = size;

	indices.resize(size * 6);

	for(int i = 0; i < m_size * 6; i += 6) {
		indices[i + 0] = vtx + 0;
		indices[i + 1] = vtx + 1;
		indices[i + 2] = vtx + 2;
		indices[i + 3] = vtx + 2;
		indices[i + 4] = vtx + 3;
		indices[i + 5] = vtx + 0;
		vtx += 4;
	}

	// Upload to GPU
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_IBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned short), &indices[0], GL_STATIC_DRAW);
	//glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

	// Allocate memory for vertex buffer.
	glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
	glBufferData(GL_ARRAY_BUFFER, m_size * sizeof(Vertex2D), NULL, GL_DYNAMIC_DRAW);
	//glBindBuffer(GL_ARRAY_BUFFER, 0);

	// Log
	log("BatchRenderer - BatchSize\n");
	log("  num sprites: %i\n", m_size);
	log("  num indices: %i\n", m_size * 6);
	log("  num vertices: %i\n", m_size);
	log("------\n");
}

void BatchRenderer2D::begin()
{
}

void BatchRenderer2D::end()
{
}

void BatchRenderer2D::submit(const Renderable2D& renderable)
{
	renderable.render(*this);
}

void BatchRenderer2D::draw(const Renderable2D* renderable)
{
	m_queue.push_back(renderable);
}

void BatchRenderer2D::render()
{
	Matrix4f MVP = m_camera ? m_camera->getProjectionViewMatrix() : Matrix4f::Identity;

	// Nothing to render.
	if (m_queue.size() < 1) {
		return;
	}

	// Bind VAO
	glBindVertexArray(m_VAO);

	// Set shader uniforms.
	m_textShader.enable();
	m_textShader.setUniform("u_MVP", MVP);
	m_textShader.disable();

	m_spriteShader.enable();
	m_spriteShader.setUniform("u_MVP", MVP);
	m_spriteShader.disable();

	prepareQueue();

	m_queue.clear();
	m_state.cleanup();
}

void BatchRenderer2D::prepareQueue()
{
	Vertex2D *buffer = static_cast<Vertex2D*>(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY));

	Batch current;

	std::sort(m_queue.begin(), m_queue.end(), compare_renderable);

	current.type = m_queue[0]->getRenderType();
	current.texture = m_queue[0]->getTexture();
	current.count = 0;
	current.offset = 0;

	for(int i = 0; i < m_queue.size(); i++) {

		const Texture *tex = m_queue[i]->getTexture();
		RenderType type = m_queue[i]->getRenderType();
		unsigned int num_vert = m_queue[i]->getVertices().size();

		// TODO: Only upload and draw if buffer is full :)
		if (current.type != type || current.texture != tex || current.count >= m_size) {
			glUnmapBuffer(GL_ARRAY_BUFFER);
			drawBatch(current);
			buffer = static_cast<Vertex2D*>(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY));

			current.type = m_queue[i]->getRenderType();
			current.count = 0;
			current.offset = 0;
			current.texture = tex;
		}

		buffer += addRenderable(buffer, m_queue[i]);
		current.count += num_vert;
	}

	glUnmapBuffer(GL_ARRAY_BUFFER);

	if (current.count > 0) {
		drawBatch(current);
	}
}

void BatchRenderer2D::uploadBatch(const Batch& batch)
{
	Vertex2D *buffer = static_cast<Vertex2D*>(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY));
	unsigned int count = 0;

	for(int i = 0; i < batch.count; i++) {
		buffer += addRenderable(buffer, m_queue[batch.offset + i]);
	}

	// Upload gpu data and draw.
	glUnmapBuffer(GL_ARRAY_BUFFER);
}

unsigned int BatchRenderer2D::addRenderable(Vertex2D* buffer, const Renderable2D* renderable)
{
	const Transform& transform = renderable->getTransform();
	const std::vector<Vertex2D>& vertices = renderable->getVertices();

	// Pretransform vertex positions to skip setting a uniform model 
	// matrix in the shader for each renderable reducing the number of draw calls.

	for(size_t i = 0; i < vertices.size(); i++) {
		buffer[i].position	= transform * vertices[i].position;
		buffer[i].color		= vertices[i].color;
		buffer[i].uv		= vertices[i].uv;
	}

	return vertices.size();
}


void BatchRenderer2D::drawBatch(Batch& batch)
{
	const ShaderProgram* shader;

	if (batch.type == RenderType_UI) {

		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

		shader = &m_textShader;
	} else {
		shader = &m_spriteShader;
	}

	// enable shader.
	m_state.enableShader(shader);

	// enable batch texture
	m_state.enableTexture(batch.texture);

	glDrawElements(GL_TRIANGLES, (batch.count / 4) * 6, GL_UNSIGNED_SHORT, 0);

	if (batch.type == RenderType_UI) {
		glDisable(GL_BLEND);
	}
}