#include "renderer.h"
#include "sprite.h"
#include "exception.h"
#include "texture_manager.h"
#include "draw.h"

using namespace Halley;

RendererLayer::RendererLayer()
	: rendered (false)
{
}


void RendererLayer::render(Renderer& r)
{
	// Render sprites
	sprites.sort([] (spSprite& a, spSprite& b) -> bool { return *a < *b; });
	for (auto iter=sprites.begin(); iter != sprites.end(); ) {
		auto next = iter;
		next++;
		auto sp = *iter;
		if (sp->isAlive()) {
			sp->render(r);
		} else {
			sprites.erase(iter);
		}
		iter = next;
	}

	// Render particles
	for (auto iter=particles.begin(); iter != particles.end(); iter++) {
		r.drawParticles(iter->second);
	}
}


/////////////////

unsigned int Renderer::vbo = 0;

Renderer::Renderer()
	: rendering(false)
{
	glGenBuffersARB(1, &vbo);
}


Renderer::~Renderer()
{
	glDeleteBuffersARB(1, &vbo);
}


void Renderer::startRendering()
{
	if (rendering) throw Exception("Already rendering!");
	rendering = true;
	for (auto iter=layers.begin(); iter != layers.end(); iter++) {
		(iter->second).rendered = false;
	}
}


void Renderer::endRendering()
{
	if (!rendering) throw Exception("Not rendering!");
	rendering = false;
	//layers.clear();
}


void Renderer::renderLayer(int n)
{
	auto iter = layers.find(n);
	if (iter != layers.end()) {
		iter->second.render(*this);
	}
}


void Renderer::renderLayers(int min, int max)
{
	for (auto iter=layers.begin(); iter != layers.end(); iter++) {
		int n = iter->first;
		if (n >= min && n <= max) {
			iter->second.render(*this);
		}
	}
}


void Renderer::renderAllLayers()
{
	for (auto iter=layers.begin(); iter != layers.end(); iter++) {
		iter->second.render(*this);
	}
}


SpriteList& Renderer::getList(spSprite sprite)
{
	int l = sprite->getLayer();
	auto& layer = layers[l];

	if (sprite->isParticle()) {
		int key = sprite->getTextureId() << 16 | sprite->getRenderType();
		return layer.particles[key];
	} else {
		return layer.sprites;
	}
}


void Renderer::addSprite(spSprite sprite)
{
	auto& list = getList(sprite);
	auto iter = list.insert(list.end(), sprite);
	sprite->iter = iter;
}


void Renderer::removeSprite(spSprite sprite)
{
// 	SpriteIter iter = sprite->iter;
// 	auto list = getList(sprite);
// 	if (iter != list.end()) {
// 		list.erase(iter);
// 	}
}


void Renderer::drawImage(Vector2f pos, Vector2f offset, int texId, double angle, double scale, int type, bool flip, Colour col)
{
	// Using VBO to draw a single object. Is this wise?
	RendererSprite s(pos.x, pos.y, offset.x, offset.y, angle, scale, flip, col);
	drawImageGroup(&s, 1, texId, type);
}


void Renderer::drawParticles(SpriteList& list)
{
	size_t n = list.size();

	if (n > 0) {
		auto head = list.front();

		std::vector<RendererSprite> s(n);
		size_t i=0;
		for (auto iter=list.begin(); iter != list.end(); ) {
			auto next = iter;
			next++;
			auto& o = *iter;
			if (o->isAlive()) {
				Vector2f pos = o->getPos();
				s[i].x = pos.x;
				s[i].y = pos.y;
				s[i].angle = o->getAngle();
				s[i].scale = o->getScale();
				s[i].ox = 0.5f;
				s[i].oy = 0.5f;
				s[i].col = Colour(1, 1, 1, 1);
				i++;
			} else {
				list.erase(iter);
			}
			iter = next;
		}
		drawImageGroup(s.data(), i, head->getTextureId(), head->getRenderType());

		//std::cout << n << " ";
	}
}


void Renderer::drawImageGroup(RendererSprite* spr, int nSprites, int texId, int type)
{
	if (nSprites == 0) return;

	// Prepare data for this group
	auto tex = TextureManager::getById(texId);
	if (!tex) 
		throw Exception("Invalid texture");
	float w = tex->GetW();
	float h = tex->GetH();

	// Bind texture and blend
	glEnable(GL_TEXTURE_2D);
	tex->Bind();
	switch (type) {
	case 0:
		SetBlendType(Texture::ALPHA_BLEND);
		break;
	case 1:
		SetBlendType(Texture::ALPHA_ADD);
		break;
	}

	// Set colour (HACK)
	Colour col = spr[0].col;
	glColor4f(col.r, col.g, col.b, col.a);

	// Prepare arrays
	size_t n = nSprites;
	size_t nNeeded = n*16;
	if (tmpData.size() < nNeeded) tmpData.resize(nNeeded*2);
	GLfloat* data = &tmpData[0];

	// Iterate through all sprites, inserting data into array
	size_t off = 0;
	size_t texOff = n*8*sizeof(GLfloat);
	for (size_t i=0; i<n; i++) {
		float scale = spr[i].scale;
		float w2 = w * scale;
		float h2 = h * scale;
		float dx = spr[i].x;
		float dy = spr[i].y;
		float ox = w2 * spr[i].ox;
		float oy = h2 * spr[i].oy;
		float a = spr[i].angle;

		if (a == 0) {
			// Not rotated
			dx -= ox;
			dy -= oy;
			GLfloat verts[] = {dx, dy, dx+w2, dy, dx+w2, dy+h2, dx, dy+h2};
			memcpy(data+off, verts, 8*sizeof(GLfloat));
		} else {
			// Rotated
			float s = sin(a);
			float c = cos(a);
			float x0 = -ox;
			float x1 = w2-ox;
			float y0 = -oy;
			float y1 = h2-oy;
			GLfloat verts[] = {	dx + x0*c - y0*s, dy + x0*s + y0*c,
								dx + x1*c - y0*s, dy + x1*s + y0*c,
								dx + x1*c - y1*s, dy + x1*s + y1*c,
								dx + x0*c - y1*s, dy + x0*s + y1*c};
			memcpy(data+off, verts, 8*sizeof(GLfloat));
		}

		float* tgt = data+n*8+off;
		tex->GetTexCoords(tgt);
		if (spr[i].flip) {
			swap(tgt[0], tgt[2]);
			swap(tgt[4], tgt[6]);
		}

		off += 8;
	}

	// Bind data to VBOs
	glBindBufferARB(GL_ARRAY_BUFFER_ARB, vbo);
	glBufferDataARB(GL_ARRAY_BUFFER_ARB, n*16*sizeof(GLfloat), data, GL_STREAM_DRAW_ARB);

	// Actually draw this shit
	glEnableClientState(GL_VERTEX_ARRAY);
	glEnableClientState(GL_TEXTURE_COORD_ARRAY);
	glVertexPointer(2, GL_FLOAT, 0, 0);
	glTexCoordPointer(2, GL_FLOAT, 0, (void*) texOff);
	glDrawArrays(GL_QUADS, 0, n*4);
	glDisableClientState(GL_VERTEX_ARRAY);
	glDisableClientState(GL_TEXTURE_COORD_ARRAY);

	// Unbind VBOs
	glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);
}
