package com.programmez.opengl.exercice2;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import static android.opengl.GLES10.*;

import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;
import android.util.Log;

public class GLTerrain {

	private IntBuffer mVertexBuffer = null;
	private IntBuffer mNormalBuffer = null;
	private IntBuffer mTextureBuffer = null;

	private final static int maxCols = 32 - 1;
	private final static int maxRows = 32 - 1;

	void loadTerrain(GL10 gl, Context context) {

		int row_vertices = 2 * (maxCols + 1);
		int nb_vertices = maxRows * row_vertices;

		int vertices[] = new int[nb_vertices * 3];
		int normals[] = new int[nb_vertices * 3];
		int texcoords[] = new int[nb_vertices * 2];

		Bitmap bmp = BitmapFactory.decodeResource(context.getResources(),
				R.drawable.heightmap_64_b);

		int index = 0;

		for (int z = 0; z < maxRows; z++) {

			// first 2 triangles of a row
			vertices[index++] = toVertexX(0);
			vertices[index++] = getPixelHeight(bmp.getPixel(0, z));
			vertices[index++] = toVertexZ(z);

			vertices[index++] = toVertexX(0);
			vertices[index++] = getPixelHeight(bmp.getPixel(0, z + 1));
			vertices[index++] = toVertexZ(z + 1);

			vertices[index++] = toVertexX(1);
			vertices[index++] = getPixelHeight(bmp.getPixel(1, z));
			vertices[index++] = toVertexZ(z);

			vertices[index++] = toVertexX(1);
			vertices[index++] = getPixelHeight(bmp.getPixel(1, z + 1));
			vertices[index++] = toVertexZ(z + 1);

			// next triangles in the row
			for (int x = 1; x < maxCols; x++) {

				// first triangle
				vertices[index++] = toVertexX(x + 1);
				vertices[index++] = getPixelHeight(bmp.getPixel(x + 1, z));
				vertices[index++] = toVertexZ(z);

				// second triangle
				vertices[index++] = toVertexX(x + 1);
				vertices[index++] = getPixelHeight(bmp.getPixel(x + 1, z + 1));
				vertices[index++] = toVertexZ(z + 1);
			}
		}

		ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
		vbb.order(ByteOrder.nativeOrder());
		mVertexBuffer = vbb.asIntBuffer();
		mVertexBuffer.put(vertices);
		mVertexBuffer.position(0);

		computeNormals(vertices, normals);
		ByteBuffer nbb = ByteBuffer.allocateDirect(normals.length * 4);
		nbb.order(ByteOrder.nativeOrder());
		mNormalBuffer = nbb.asIntBuffer();
		mNormalBuffer.put(normals);
		mNormalBuffer.position(0);

		computeTexture(vertices, texcoords);
		ByteBuffer tbb = ByteBuffer.allocateDirect(texcoords.length * 4);
		tbb.order(ByteOrder.nativeOrder());
		mTextureBuffer = tbb.asIntBuffer();
		mTextureBuffer.put(texcoords);
		mTextureBuffer.position(0);

		bmp.recycle();
	}

	private void computeTexture(int[] vertices, int[] texcoords) {

		int tex = 0;
		for (int i = 0; i < vertices.length / 3; i++) {
			int index = i * 3;
			float x = (fixedToFloat(vertices[index]) + maxCols / 2) / maxCols;
			float z = (fixedToFloat(vertices[index + 2]) + maxRows / 2)
					/ maxRows;
			//divided by 2 because I take half of the heightmap
			// seems to be a bug in android, but may be in this code as well
			texcoords[tex++] = floatToFixed(x/2f);
			texcoords[tex++] = floatToFixed(z/2f);
		}
	}

	private void computeNormals(int[] vertices, int[] normals) {

		int vertPerRow = 3 + (2 * maxCols) - 1;

		for (int row = 0; row <= maxRows; row++) {
			for (int col = 0; col <= maxCols; col++) {
				// 4 corners
				if ((row == 0) && (col == 0)) {

					float norm1[] = computeTriangleNormal(vertices, 0);
					normals[0] = floatToFixed(norm1[0]);
					normals[1] = floatToFixed(norm1[1]);
					normals[2] = floatToFixed(norm1[2]);

				} else if ((row == 0) && (col == maxCols)) {

					int index = vertPerRow - 2;
					float norm1[] = computeTriangleNormal(vertices, index - 2);
					float norm2[] = computeTriangleNormal(vertices, index - 1);

					int indexNorm = index * 3;
					normals[indexNorm] = floatToFixed((norm1[0] + norm2[0]) / 2);
					normals[indexNorm + 1] = floatToFixed((norm1[1] + norm2[1]) / 2);
					normals[indexNorm + 2] = floatToFixed((norm1[2] + norm2[2]) / 2);

				} else if ((row == maxRows) && (col == 0)) {
					int index = ((maxRows - 1) * vertPerRow) + 1;
					float norm1[] = computeTriangleNormal(vertices, index - 1);
					float norm2[] = computeTriangleNormal(vertices, index);

					int indexNorm = index * 3;
					normals[indexNorm] = floatToFixed((norm1[0] + norm2[0]) / 2);
					normals[indexNorm + 1] = floatToFixed((norm1[1] + norm2[1]) / 2);
					normals[indexNorm + 2] = floatToFixed((norm1[2] + norm2[2]) / 2);

				}
				// 2 adjacent triangles
				else if ((row == maxRows) && (col == maxCols)) {
					int index = (maxRows * vertPerRow) - 1;
					float norm1[] = computeTriangleNormal(vertices, index - 3);
					float norm2[] = computeTriangleNormal(vertices, index - 2);

					int indexNorm = index * 3;
					normals[indexNorm] = floatToFixed((norm1[0] + norm2[0]) / 2);
					normals[indexNorm + 1] = floatToFixed((norm1[1] + norm2[1]) / 2);
					normals[indexNorm + 2] = floatToFixed((norm1[2] + norm2[2]) / 2);

				}
				// bottom line, no corner
				// 3 adjacent triangles
				else if ((row == 0) && (col > 0) && (col < maxCols)) {
					int index = col * 2;
					float norm1[] = computeTriangleNormal(vertices, index - 2);
					float norm2[] = computeTriangleNormal(vertices, index - 1);
					float norm3[] = computeTriangleNormal(vertices, index);

					int indexNorm = index * 3;
					normals[indexNorm] = floatToFixed((norm1[0] + norm2[0] + norm3[0]) / 3);
					normals[indexNorm + 1] = floatToFixed((norm1[1] + norm2[1] + norm3[1]) / 3);
					normals[indexNorm + 2] = floatToFixed((norm1[2] + norm2[2] + norm3[2]) / 3);

				}
				// top line, no corner, 3 adjacent triangles
				else if ((row == maxRows) && (col > 0) && (col < maxCols)) {
					int index = ((maxRows - 1) * vertPerRow) + col * 2 + 1;
					float norm1[] = computeTriangleNormal(vertices, index - 2);
					float norm2[] = computeTriangleNormal(vertices, index - 1);
					float norm3[] = computeTriangleNormal(vertices, index);

					int indexNorm = index * 3;
					normals[indexNorm] = floatToFixed((norm1[0] + norm2[0] + norm3[0]) / 3);
					normals[indexNorm + 1] = floatToFixed((norm1[1] + norm2[1] + norm3[1]) / 3);
					normals[indexNorm + 2] = floatToFixed((norm1[2] + norm2[2] + norm3[2]) / 3);
				}
				// left side, no corner, 3 adjacent triangles
				else if ((col == 0) && (row > 0) && (row < maxRows)) {

					int index = ((row - 1) * vertPerRow) + 1;
					float norm1[] = computeTriangleNormal(vertices, index - 1);
					float norm2[] = computeTriangleNormal(vertices, index);
					float norm3[] = computeTriangleNormal(vertices, index
							+ vertPerRow - 1);

					int indexNorm = index * 3;
					normals[indexNorm] = floatToFixed((norm1[0] + norm2[0] + norm3[0]) / 3);
					normals[indexNorm + 1] = floatToFixed((norm1[1] + norm2[1] + norm3[1]) / 3);
					normals[indexNorm + 2] = floatToFixed((norm1[2] + norm2[2] + norm3[2]) / 3);

					// repeat for 'double' vertex
					indexNorm = (index + vertPerRow - 1) * 3;
					normals[indexNorm] = floatToFixed((norm1[0] + norm2[0] + norm3[0]) / 3);
					normals[indexNorm + 1] = floatToFixed((norm1[1] + norm2[1] + norm3[1]) / 3);
					normals[indexNorm + 2] = floatToFixed((norm1[2] + norm2[2] + norm3[2]) / 3);

				}

				// right side, no corner, 3 adjacent triangles
				else if ((col == maxCols) && (row > 0) && (row < maxRows)) {

					int index = (row * vertPerRow) - 1;
					float norm1[] = computeTriangleNormal(vertices, index - 2);
					float norm2[] = computeTriangleNormal(vertices, index
							+ vertPerRow - 3);
					float norm3[] = computeTriangleNormal(vertices, index
							+ vertPerRow - 2);

					int indexNorm = index * 3;
					normals[indexNorm] = floatToFixed((norm1[0] + norm2[0] + norm3[0]) / 3);
					normals[indexNorm + 1] = floatToFixed((norm1[1] + norm2[1] + norm3[1]) / 3);
					normals[indexNorm + 2] = floatToFixed((norm1[2] + norm2[2] + norm3[2]) / 3);

					indexNorm = (index + vertPerRow - 1) * 3;
					normals[indexNorm] = floatToFixed((norm1[0] + norm2[0] + norm3[0]) / 3);
					normals[indexNorm + 1] = floatToFixed((norm1[1] + norm2[1] + norm3[1]) / 3);
					normals[indexNorm + 2] = floatToFixed((norm1[2] + norm2[2] + norm3[2]) / 3);
				}

				// all other cases, vertices not on border or corner
				// 6 adjacent triangles
				else {

					int index = ((row - 1) * vertPerRow) + (col * 2) + 1;
					float norm1[] = computeTriangleNormal(vertices, index - 2);
					float norm2[] = computeTriangleNormal(vertices, index - 1);
					float norm3[] = computeTriangleNormal(vertices, index);

					// second half
					int index2 = index + vertPerRow - 1;
					float norm4[] = computeTriangleNormal(vertices, index2 - 2);
					float norm5[] = computeTriangleNormal(vertices, index2 - 1);
					float norm6[] = computeTriangleNormal(vertices, index2);

					int indexNorm = index * 3;
					normals[indexNorm] = floatToFixed((norm1[0] + norm2[0]
							+ norm3[0] + norm4[0] + norm5[0] + norm6[0]) / 6);
					normals[indexNorm + 1] = floatToFixed((norm1[1] + norm2[1]
							+ norm3[1] + norm4[1] + norm5[1] + norm6[1]) / 6);
					normals[indexNorm + 2] = floatToFixed((norm1[2] + norm2[2]
							+ norm3[2] + norm4[2] + norm5[2] + norm6[2]) / 6);

					// repeat for 'double' vertex
					indexNorm = index2 * 3;
					normals[indexNorm] = floatToFixed((norm1[0] + norm2[0]
							+ norm3[0] + norm4[0] + norm5[0] + norm6[0]) / 6);
					normals[indexNorm + 1] = floatToFixed((norm1[1] + norm2[1]
							+ norm3[1] + norm4[1] + norm5[1] + norm6[1]) / 6);
					normals[indexNorm + 2] = floatToFixed((norm1[2] + norm2[2]
							+ norm3[2] + norm4[2] + norm5[2] + norm6[2]) / 6);
				}
			}
		}
	}

	private float[] computeTriangleNormal(int[] vertices, int i) {
		
		float[] result = new float[3];
		int index = i * 3;

		float p1x = fixedToFloat(vertices[index]);
		float p1y = fixedToFloat(vertices[index + 1]);
		float p1z = fixedToFloat(vertices[index + 2]);

		index += 3;

		float p2x = fixedToFloat(vertices[index]);
		float p2y = fixedToFloat(vertices[index + 1]);
		float p2z = fixedToFloat(vertices[index + 2]);

		index += 3;

		float p3x = fixedToFloat(vertices[index]);
		float p3y = fixedToFloat(vertices[index + 1]);
		float p3z = fixedToFloat(vertices[index + 2]);

		// U = p2-p1
		// V = p3-p1
		float ux = p2x - p1x;
		float uy = p2y - p1y;
		float uz = p2z - p1z;

		float vx = p3x - p1x;
		float vy = p3y - p1y;
		float vz = p3z - p1z;

		result[0] = uy * vz - uz * vy;
		result[1] = uz * vx - ux * vz;
		result[2] = ux * vy - uy * vx;

		return result;
	}

	private float fixedToFloat(int i) {
		float fp = i;
		fp = fp / 65536f;
		return fp;
	}

	int getPixelHeight(int pixelValue) {
		int lowByte = (pixelValue >> 8) & 0x000000FF;
		int middleByte = (pixelValue >> 16) & 0x000000FF;
		int highByte = (pixelValue >> 24) & 0x000000FF;
		int average = (lowByte + middleByte + highByte) / 3;
		average = average / 10;
		average = average * 65536;

		return average;
	}

	int toVertexX(int val) {
		int val2 = val - (maxCols / 2);
		return val2 * 65536;
	}

	int toVertexZ(int val) {
		int val2 = val - (maxRows / 2);
		return val2 * 65536;
	}

	int floatToFixed(float val) {
		return (int) (val * 65536f);
	}

	void loadTexture(GL10 gl, Context context) {
		Bitmap bmp = BitmapFactory.decodeResource(context.getResources(),
				R.drawable.texture_256);
		GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bmp, 0);
		gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,
				GL10.GL_LINEAR);
		gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,
				GL10.GL_LINEAR);
		bmp.recycle();
	}

	public void draw(GL10 gl) {
		glEnable(GL_CULL_FACE);
		glCullFace(GL_BACK);
		glVertexPointer(3, GL10.GL_FIXED, 0, mVertexBuffer);
		glTexCoordPointer(2, GL10.GL_FIXED, 0, mTextureBuffer);
		glNormalPointer(GL10.GL_FIXED, 0, mNormalBuffer);

		int index = 0;
		for (int x = 0; x < maxRows; x++) {
			glDrawArrays(GL10.GL_TRIANGLE_STRIP, index, 2 * (maxCols + 1));
			index += 2 * (maxCols + 1);
		}
	}
}
