#ifndef GLTRIANGLES_H
#define GLTRIANGLES_H

#include <QOpenGLFunctions>
#include "GL.h"
#include "GLHelper.h"

#include <type_traits>
#include <Indoor/geo/Point3.h>

template <typename T> class GLTriangles : protected QOpenGLFunctions {

private:

	QOpenGLBuffer arrayBuf;
	QOpenGLBuffer indexBuf;
	QOpenGLTexture* textures[4];

	std::vector<T> vertices;
	std::vector<GLuint> indices;

public:

	/** ctor */
	GLTriangles() : arrayBuf(QOpenGLBuffer::VertexBuffer), indexBuf(QOpenGLBuffer::IndexBuffer), textures() {
		;
	}

	/** dtor */
	~GLTriangles() {

		arrayBuf.destroy();
		indexBuf.destroy();

		for (int i = 0; i < 4; ++i) {delete textures[i];}

	}

	/** set the to-be-used texture */
	void setTexture(const int slot, const QString& textureFile) {

		const QImage img(textureFile);
		if (img.width() <= 0) {throw "error";}

		QOpenGLTexture* texture = new QOpenGLTexture(img);
		texture->setMinificationFilter(QOpenGLTexture::Linear);
		texture->setMagnificationFilter(QOpenGLTexture::Linear);
		texture->setWrapMode(QOpenGLTexture::Repeat);
		texture->generateMipMaps();
		textures[slot] = texture;

	}

	void setDiffuse(const QString& textureFile) {
		setTexture(0, textureFile);
	}

	void setNormalMap(const QString& textureFile) {
		setTexture(1, textureFile);
	}

	/** add a new face to this element */
	void addFace(const T& vnt1, const T& vnt2, const T& vnt3) {
		addFace(vnt1, vnt2, vnt3, 0);
	}

	/** add a new face to this element */
	void addFaceCCW(const T& vnt1, const T& vnt2, const T& vnt3) {
		addFace(vnt1, vnt2, vnt3, 1);
	}

	/** add a new face to this element */
	void addFaceCW(const T& vnt1, const T& vnt2, const T& vnt3) {
		addFace(vnt1, vnt2, vnt3, 2);
	}

	/** add a new quad to this element */
	void addQuadCCW(const T& vnt1, const T& vnt2, const T& vnt3, const T& vnt4) {
		addFace(vnt1, vnt2, vnt3, 1);
		addFace(vnt3, vnt4, vnt1, 1);
	}

	/** add a new quad to this element */
	void addQuadCW(const T& vnt1, const T& vnt2, const T& vnt3, const T& vnt4) {
		addFace(vnt1, vnt2, vnt3, 2);
		addFace(vnt3, vnt4, vnt1, 2);
	}

	/** add a new quad to this element */
	void addQuad(const T& vnt1, const T& vnt2, const T& vnt3, const T& vnt4) {
		addFace(vnt1, vnt2, vnt3, 0);
		addFace(vnt3, vnt4, vnt1, 0);
	}

	/** build the underlying buffers */
	void build() {

		initializeOpenGLFunctions();

		// Transfer vertex data to VBO 0
		arrayBuf.create();
		arrayBuf.bind();
		arrayBuf.allocate(vertices.data(), vertices.size() * sizeof(vertices[0]));

		// Transfer index data to VBO 1
		indexBuf.create();
		indexBuf.bind();
		indexBuf.allocate(indices.data(), indices.size() * sizeof(indices[0]));

	}

	void rebuild() {
		if (indexBuf.isCreated()) {indexBuf.destroy();}
		if (arrayBuf.isCreated()) {arrayBuf.destroy();}
		build();
	}

	void clear() {
		indices.clear();
		vertices.clear();
	}

	/** render the element */
	void render(QOpenGLShaderProgram* program) {

		// nothing to-do?
		if (indices.empty()) {return;}

		// Tell OpenGL which VBOs to use
		arrayBuf.bind();
		indexBuf.bind();

		for (int i = 0; i < 4; ++i) {
			if (textures[i]) { textures[i]->bind(i); }
		}

		// vertices
		int vertLoc = program->attributeLocation("a_position");
		program->enableAttributeArray(vertLoc);
		program->setAttributeBuffer(vertLoc, GL_FLOAT, vertices[0].getVertOffset(), 3, sizeof(vertices[0]));

		// Tell OpenGL programmable pipeline how to locate vertex texture coordinate data
		if (T::hasNormal()) {
			int normLoc = program->attributeLocation("a_normal");
			program->enableAttributeArray(normLoc);
			program->setAttributeBuffer(normLoc, GL_FLOAT, vertices[0].getNormOffset(), 3, sizeof(vertices[0]));
		}

		if (T::hasTexCoord()) {
			int texcoordLocation = program->attributeLocation("a_texcoord");
			program->enableAttributeArray(texcoordLocation);
			program->setAttributeBuffer(texcoordLocation, GL_FLOAT, vertices[0].getTexOffset(), 2, sizeof(vertices[0]));
		}

		// bind tangent data?
		if (T::hasTangent()) {
			int tanLocation = program->attributeLocation("a_tangent");
			program->enableAttributeArray(tanLocation);
			program->setAttributeBuffer(tanLocation, GL_FLOAT, vertices[0].getTanOffset(), 3, sizeof(vertices[0]));
		}

		// bind color data?
		if (T::hasColor()) {
			int colorLoc = program->attributeLocation("a_color");
			program->enableAttributeArray(colorLoc);
			program->setAttributeBuffer(colorLoc, GL_FLOAT, vertices[0].getColorOffset(), 3, sizeof(vertices[0]));
		}

		// texture
		program->setUniformValue("texture", 0);

		// Draw cube geometry using indices from VBO 1
		glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);

	}

private:

	void addFace(const T& vnt1, const T& vnt2, const T& vnt3, const int mode) {

		// add vertices (remove duplicates!)
		const int i1 = addOnce(vnt1);
		const int i2 = addOnce(vnt2);
		const int i3 = addOnce(vnt3);

		// get current orientation
		const bool ccw = GLHelper::isCCW(vnt1.vert, vnt2.vert, vnt3.vert);

		// create indices
		if (mode == 0 || (mode == 1 && ccw) || (mode == 2 && !ccw) ) {
			indices.push_back(i1);
			indices.push_back(i2);
			indices.push_back(i3);
		} else {
			indices.push_back(i3);
			indices.push_back(i2);
			indices.push_back(i1);
		}

	}

	/** to conserve memory, avoid duplicate VNTs! */
	int addOnce(const T& vnt) {

		const auto it = std::find(vertices.begin(), vertices.end(), vnt);

		if (it == vertices.end()) {
			const int idx = vertices.size();
			vertices.push_back(vnt);
			return idx;
		} else {
			const int idx = it - vertices.begin();
			return idx;
		}

	}




};

#endif // GLTRIANGLES_H
