#include "stdafx.h"
#include "Graphics.h"
#include "Surface.h"
#include "Exception.h"
#include "Core/Graphics/Image.h"

#ifdef GUI_GTK

namespace gui {

	GLGraphics::GLGraphics(GLSurface &surface, Nat id) : Graphics3D(id), surface(surface) {
		renderParams = new (this) GLParams(this);
	}

	void GLGraphics::makeCurrent() {
		surface.makeCurrent();
		glBindFramebuffer(GL_FRAMEBUFFER, surface.framebuffer);
	}

	void GLGraphics::beforeRender() {
	}

	Bool GLGraphics::afterRender() {
		return true;
	}

	void GLGraphics::fill(Color color) {
		makeCurrent();
		glClearColor(color.r, color.g, color.b, color.a);
		glClear(GL_COLOR_BUFFER_BIT);
	}

	void GLGraphics::clearDepth() {
		makeCurrent();
		glClearDepth(1.0f);
		glClear(GL_DEPTH_BUFFER_BIT);
	}

	Graphics3D::RenderParams *GLGraphics::beginRender(Pipeline *pipeline) {
		renderParams->clear();
		renderParams->program = (GLProgram *)pipeline->forGraphics(this);
		renderParams->program->update(pipeline, DirtyRange(0));

		// Bind textures.
		size_t texCount = renderParams->textures ? renderParams->textures->count() : 0;
		if (pipeline->textureCount() > texCount)
			renderParams->textures = runtime::allocArray<GLTexture *>(engine(), &pointerArrayType, pipeline->textureCount());

		for (size_t i = 0; i < pipeline->textureCount(); i++) {
			Texture *t = pipeline->texture(i);
			renderParams->textures->v[i] = (GLTexture *)t->forGraphics(this);
		}

		if (renderParams->textures)
			renderParams->textures->filled = pipeline->textureCount();

		return renderParams;
	}

	GLGraphics::GLParams::GLParams(GLGraphics *graphics) : graphics(graphics) {
		buffers = runtime::allocArray<GLBuffer *>(engine(), &pointerArrayType, 16);
		textures = null;
	}

	void GLGraphics::GLParams::bind(Nat id, VertexBufferBase *vbuf) {
		if (id < buffers->count()) {
			// Fast path.

			if (!indexBuffer) {
				if (buffers->filled == 0)
					numVertices = vbuf->count();
				else
					numVertices = min(numVertices, vbuf->count());
			}

			buffers->v[id] = (GLBuffer *)vbuf->forGraphics(graphics);
			buffers->filled = max(buffers->filled, size_t(id) + 1);
		} else {
			// Slow path. Try to nudge the compiler to put it last.
			GcArray<GLBuffer *> *copy =
				runtime::allocArray<GLBuffer *>(engine(), &pointerArrayType,
												max(buffers->count() * 2, size_t(id + 1)));
			memcpy(copy->v, buffers->v, sizeof(GLBuffer *) * buffers->count());
			copy->filled = buffers->filled;
			buffers = copy;

			bind(id, vbuf);
		}
	}

	void GLGraphics::GLParams::bind(IndexBufferBase *ibuf) {
		indexBuffer = (GLBuffer *)ibuf->forGraphics(graphics);
		numVertices = ibuf->count();
		topology = ibuf->primitive;
	}

	void GLGraphics::GLParams::primitive(DrawPrimitive p) {
		topology = p;
	}

	void GLGraphics::GLParams::clear() {
		for (size_t i = 0; i < buffers->filled; i++)
			buffers->v[i] = null;
		buffers->filled = 0;

		if (textures) {
			for (size_t i = 0; i < textures->filled; i++)
				textures->v[i] = null;
			textures->filled = 0;
		}

		program = null;
		numVertices = 0;
		indexBuffer = null;
		topology = drawTriangles;
	}

	static GLenum glTopology(DrawPrimitive p) {
		switch (p) {
		case drawTriangles:
			return GL_TRIANGLES;
		case drawTriangleStrip:
			return GL_TRIANGLE_STRIP;
		case drawLines:
			return GL_LINES;
		case drawLineStrip:
			return GL_LINE_STRIP;
		case drawPoints:
			return GL_POINTS;
		default:
			return GL_TRIANGLES;
		}
	}

	void GLGraphics::GLParams::done() {
		if (!program) {
			clear();
			return;
		}

		graphics->surface.makeCurrent();

		// Bind the buffer to the format in the vertex shader.
		for (size_t i = 0; i < buffers->filled; i++) {
			GLBuffer *buffer = buffers->v[i];
			if (!buffer)
				continue;

			glVertexArrayVertexBuffer(program->inputFormat, i /* index */,
									buffer->buffer, 0 /* offset */, buffer->elementSize);
		}

		if (textures) {
			for (size_t i = 0; i < textures->filled; i++) {
				GLTexture *texture = textures->v[i];
				if (!texture)
					continue;

				glActiveTexture(GL_TEXTURE0 + i);
				glBindTexture(GL_TEXTURE_2D, texture->handle);
			}
		}

		// Configuration:
		glEnable(GL_DEPTH_TEST);
		glDepthFunc(GL_LESS);
		glEnable(GL_CULL_FACE);
		glCullFace(GL_BACK);
		glFrontFace(GL_CW); // Note: this is likely because we use a left-handed coordinate system.

		// Draw!
		glBindVertexArray(program->inputFormat);
		for (size_t i = 0; i < program->constantBuffers.size(); i++)
			glBindBufferBase(GL_UNIFORM_BUFFER, i, program->constantBuffers[i]);
		glUseProgram(program->program);

		GLenum mode = glTopology(topology);
		if (indexBuffer) {
			// To handle restarts properly.
			glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer->buffer);
			glDrawElements(mode, numVertices, GL_UNSIGNED_INT, NULL);
		} else {
			glDrawArrays(mode, 0, numVertices);
		}

		clear();
	}

	void GLGraphics::createResource(Resource3D *resource, void *&data, Resource3D::Cleanup &cleanup) {
		if (Pipeline *p = as<Pipeline>(resource)) {
			VertexShader *vertexShader = p->vertexShader();
			PixelShader *pixelShader = p->pixelShader();

			GLVertexShader *vs = (GLVertexShader *)vertexShader->forGraphics(this);
			GLPixelShader *ps = (GLPixelShader *)pixelShader->forGraphics(this);

			data = new GLProgram(vs, ps, vertexShader->inputDescription(), p);
			cleanup = &GLProgram::destroy;
		} else if (VertexShader *vs = as<VertexShader>(resource)) {
			data = new GLVertexShader(surface, vs);
			cleanup = &GLVertexShader::destroy;
		} else if (PixelShader *s = as<PixelShader>(resource)) {
			data = new GLPixelShader(surface, s);
			cleanup = &GLPixelShader::destroy;
		} else if (VertexBufferBase *vb = as<VertexBufferBase>(resource)) {
			data = new GLBuffer(surface, vb);
			cleanup = &GLBuffer::destroy;
		} else if (IndexBufferBase *ib = as<IndexBufferBase>(resource)) {
			data = new GLBuffer(surface, ib);
			cleanup = &GLBuffer::destroy;
		} else if (Texture *tex = as<Texture>(resource)) {
			data = new GLTexture(surface, tex);
			cleanup = &GLTexture::destroy;
		} else {
			throw new (this) NotSupported(S("Can not create this resource type."));
		}
	}

	void GLGraphics::updateResource(Resource3D *resource, void *data, const DirtyRange &dirty) {
		if (VertexBufferBase *vb = as<VertexBufferBase>(resource)) {
			((GLBuffer *)data)->update(vb, dirty);
		} else if (IndexBufferBase *ib = as<IndexBufferBase>(resource)) {
			((GLBuffer *)data)->update(ib, dirty);
		} else if (Pipeline *p = as<Pipeline>(resource)) {
			((GLProgram *)data)->update(p, dirty);
		} else if (Texture *tex = as<Texture>(resource)) {
			((GLTexture *)data)->update(tex);
		} else {
			throw new (this) NotSupported(S("Can not update this resource type."));
		}
	}

}

#else

namespace gui {

	GLGraphics::GLParams::GLParams(GLGraphics *graphics) : graphics(graphics), buffers(null) {}

	GLGraphics::GLGraphics(GLSurface &surface, Nat id) : Graphics3D(id), surface(surface) {
		renderParams = new (this) GLParams(this);
	}

	GRAPHICS3D_DEFINES(GLGraphics, GLGraphics::GLParams, renderParams)

}

#endif
