package net.npe.texturedcube.client;

import com.google.gwt.canvas.client.Canvas;
import com.google.gwt.canvas.dom.client.CanvasPixelArray;
import com.google.gwt.canvas.dom.client.Context2d;
import com.google.gwt.canvas.dom.client.ImageData;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.typedarrays.client.Uint8ArrayNative;
import com.google.gwt.typedarrays.shared.ArrayBuffer;
import com.google.gwt.user.client.ui.RootLayoutPanel;
import com.google.gwt.xhr.client.ReadyStateChangeHandler;
import com.google.gwt.xhr.client.XMLHttpRequest;
import com.google.gwt.xhr.client.XMLHttpRequest.ResponseType;
import com.googlecode.gwtgl.array.Float32Array;
import com.googlecode.gwtgl.binding.WebGLBuffer;
import com.googlecode.gwtgl.binding.WebGLProgram;
import com.googlecode.gwtgl.binding.WebGLRenderingContext;
import com.googlecode.gwtgl.binding.WebGLShader;
import com.googlecode.gwtgl.binding.WebGLTexture;
import com.googlecode.gwtgl.binding.WebGLUniformLocation;

import static com.googlecode.gwtgl.binding.WebGLRenderingContext.*;

/**
 * Entry point classes define <code>onModuleLoad()</code>.
 */
public class TexturedCube implements EntryPoint {
	
	private float [] VERTICES = {
			// position.xyz, texcoord.xy
			// front
			-0.5f, +0.5f, +0.5f, 0.f, 0.f,
			-0.5f, -0.5f, +0.5f, 0.f, 1.f,
			+0.5f, +0.5f, +0.5f, 1.f, 0.f,
			+0.5f, -0.5f, +0.5f, 1.f, 1.f,
			// back
			+0.5f, +0.5f, -0.5f, 0.f, 0.f,
			+0.5f, -0.5f, -0.5f, 0.f, 1.f,
			-0.5f, +0.5f, -0.5f, 1.f, 0.f,
			-0.5f, -0.5f, -0.5f, 1.f, 1.f,
	};
	
	private String VERTEX_SHADER_SOURCE =
			"attribute vec3 position;"+
			"attribute vec2 texcoord;"+
			"varying vec2 vTexcoord;"+
			"uniform mat4 MVP;"+
			"void main() { gl_Position = MVP * vec4(position, 1.0); vTexcoord = texcoord; }";
	
	private String FRAGMENT_SHADER_SOURCE =
			"precision highp float;"+
			"varying vec2 vTexcoord;"+
			"uniform sampler2D texture;"+
			"void main() { gl_FragColor = texture2D(texture, vTexcoord); }";
	
	private float [][] MODELMATRICES = {
			{ // none
				1, 0, 0, 0,
				0, 1, 0, 0,
				0, 0, 1, 0,
				0, 0, 0, 1,
			},
			{ // rotY90
				0, 0, -1, 0,
				0, 1, 0, 0,
				1, 0, 0, 0,
				0, 0, 0, 1,
			},
			{ // rotX-90
				1, 0, 0, 0,
				0, 0, -1, 0,
				0, 1, 0, 0,
				0, 0, 0, 1,
			},
	};

	public static final String [] TEXTURE_URLS = {
		"images/rgb_rle_LL.tga",
		"images/grayscale_rle_LL.tga",
		"images/indexed_rle_LL.tga",
	};
	
	public void onModuleLoad() {
		
		Canvas canvas = Canvas.createIfSupported();
		canvas.setStyleName("MyCanvas");
		canvas.setCoordinateSpaceWidth(400);
		canvas.setCoordinateSpaceHeight(400);
		RootLayoutPanel.get().add(canvas);
		
		gl = (WebGLRenderingContext)canvas.getContext("experimental-webgl");
		gl.viewport(0, 0, 400, 400);
		
		WebGLBuffer vertexBuffer = gl.createBuffer();
		gl.bindBuffer(ARRAY_BUFFER, vertexBuffer);
		gl.bufferData(ARRAY_BUFFER, Float32Array.create(VERTICES), STATIC_DRAW);

		WebGLShader vertexShader = gl.createShader(VERTEX_SHADER);
		gl.shaderSource(vertexShader, VERTEX_SHADER_SOURCE);
		gl.compileShader(vertexShader);
		
		WebGLShader fragmentShader = gl.createShader(FRAGMENT_SHADER);
		gl.shaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE);
		gl.compileShader(fragmentShader);
		
		program = gl.createProgram();
		gl.attachShader(program, vertexShader);
		gl.attachShader(program, fragmentShader);
		gl.linkProgram(program);
		
		gl.useProgram(program);
		gl.bindBuffer(ARRAY_BUFFER, vertexBuffer);
		
		WebGLUniformLocation texture = gl.getUniformLocation(program, "texture");
		gl.uniform1i(texture, 0);
		
		int posAttr = gl.getAttribLocation(program, "position");
		gl.vertexAttribPointer(posAttr, 3, FLOAT, false, 5*4, 0);
		gl.enableVertexAttribArray(posAttr);
		
		int texAttr = gl.getAttribLocation(program, "texcoord");
		gl.vertexAttribPointer(texAttr, 2, FLOAT, false, 5*4, 3*4);
		gl.enableVertexAttribArray(texAttr);
		
		for(int i=0; i<TEXTURE_URLS.length; i++) {
			loadTexture(TEXTURE_URLS[i], i);
		}
		
	}
	
	void draw() {
		
		gl.clearColor(0.5f, 0.5f, 0.5f, 1);
		gl.clear(COLOR_BUFFER_BIT);
		gl.enable(CULL_FACE);
		gl.cullFace(BACK);
		gl.enable(BLEND);
		gl.blendFunc(SRC_ALPHA, ONE_MINUS_SRC_ALPHA);
		
		float [] view = new float[16];
		lookAt(view, 2, 2, 2, 0, 0, 0, 0, 1, 0);
		
		float [] proj = new float[16];
		perspective(proj, 45, 1.f, 1.f, 10.f);
		
		float [] vp = new float[16];
		multiply(vp, view, proj);
		
		float [] mvp = new float[16];
		for(int i=0; i<3; i++) {
			multiply(mvp, MODELMATRICES[i], vp);
			draw(mvp, textures[i]);
		}
		
	}
	
	void draw(float [] mvp, WebGLTexture texture) {
		
		WebGLUniformLocation MVP = gl.getUniformLocation(program, "MVP");
		gl.uniformMatrix4fv(MVP, false, mvp);
		
		gl.bindTexture(TEXTURE_2D, texture);
		gl.activeTexture(TEXTURE0);
		
		gl.drawArrays(TRIANGLE_STRIP, 0, 4);
		gl.drawArrays(TRIANGLE_STRIP, 4, 4);
		
	}
	
	Canvas createImageCanvas(int [] pixels, int width, int height) {

	    Canvas canvas = Canvas.createIfSupported();
	    canvas.setCoordinateSpaceWidth(width);
	    canvas.setCoordinateSpaceHeight(height);

	    Context2d context = canvas.getContext2d();
	    ImageData data = context.createImageData(width, height);

	    CanvasPixelArray array = data.getData();
	    for(int i=0; i<width*height; i++) {
	        array.set(4*i+0, pixels[i] & 0xFF);
	        array.set(4*i+1, (pixels[i] >> 8) & 0xFF);
	        array.set(4*i+2, (pixels[i] >> 16) & 0xFF);
	        array.set(4*i+3, (pixels[i] >> 24) & 0xFF);
	    }
	    context.putImageData(data, 0, 0);

	    return canvas;

	}
	
	void loadTexture(String url, int index) {
		final int i = index;
	    XMLHttpRequest request = XMLHttpRequest.create();
	    request.open("GET", url);
	    request.setResponseType(ResponseType.ArrayBuffer);
	    request.setOnReadyStateChange(new ReadyStateChangeHandler() {
	        @Override
	        public void onReadyStateChange(XMLHttpRequest xhr) {
	            if(xhr.getReadyState() == XMLHttpRequest.DONE) {
	                if(xhr.getStatus() >= 400) {
	                    // error
	                    System.out.println("Error");
	                }
	                else {
	                	try {
		                	ArrayBuffer arrayBuffer = xhr.getResponseArrayBuffer();
		    				Uint8ArrayNative u8array = Uint8ArrayNative.create(arrayBuffer);
		    				byte [] buffer = new byte[u8array.length()];
		    				for(int i=0; i<buffer.length; i++) {
		    					buffer[i] = (byte)u8array.get(i);
		    				}
		    				
		    				int [] pixels = TGAReader.read(buffer, TGAReader.ABGR);
		    				int width = TGAReader.getWidth(buffer);
		    				int height = TGAReader.getHeight(buffer);
		    				
		    				Canvas canvas = createImageCanvas(pixels, width, height);
		    				
		    				WebGLTexture texture = gl.createTexture();
		    				gl.enable(TEXTURE_2D);
		    				gl.bindTexture(TEXTURE_2D, texture);
		    				
		    				gl.texImage2D(TEXTURE_2D, 0, RGBA, RGBA, UNSIGNED_BYTE, canvas.getElement());
	
		    				gl.texParameteri(TEXTURE_2D, TEXTURE_WRAP_S, CLAMP_TO_EDGE);
		    				gl.texParameteri(TEXTURE_2D, TEXTURE_WRAP_T, CLAMP_TO_EDGE);
		    				gl.texParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, LINEAR);
		    				gl.texParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, LINEAR);
		    				
		    				textures[i] = texture;
		    				draw();
	                	}
	                	catch(Exception e) {
	                		e.printStackTrace();
	                	}
	                }
	            }
	        }
	    });
	    request.send();
	}
	
	public static void multiply(float [] m0, float [] m1, float [] m2) {
		
		m0[ 0] = m1[ 0]*m2[ 0] + m1[ 1]*m2[ 4] + m1[ 2]*m2[ 8] + m1[ 3]*m2[12];
		m0[ 1] = m1[ 0]*m2[ 1] + m1[ 1]*m2[ 5] + m1[ 2]*m2[ 9] + m1[ 3]*m2[13];
		m0[ 2] = m1[ 0]*m2[ 2] + m1[ 1]*m2[ 6] + m1[ 2]*m2[10] + m1[ 3]*m2[14];
		m0[ 3] = m1[ 0]*m2[ 3] + m1[ 1]*m2[ 7] + m1[ 2]*m2[11] + m1[ 3]*m2[15];

		m0[ 4] = m1[ 4]*m2[ 0] + m1[ 5]*m2[ 4] + m1[ 6]*m2[ 8] + m1[ 7]*m2[12];
		m0[ 5] = m1[ 4]*m2[ 1] + m1[ 5]*m2[ 5] + m1[ 6]*m2[ 9] + m1[ 7]*m2[13];
		m0[ 6] = m1[ 4]*m2[ 2] + m1[ 5]*m2[ 6] + m1[ 6]*m2[10] + m1[ 7]*m2[14];
		m0[ 7] = m1[ 4]*m2[ 3] + m1[ 5]*m2[ 7] + m1[ 6]*m2[11] + m1[ 7]*m2[15];
		
		m0[ 8] = m1[ 8]*m2[ 0] + m1[ 9]*m2[ 4] + m1[10]*m2[ 8] + m1[11]*m2[12];
		m0[ 9] = m1[ 8]*m2[ 1] + m1[ 9]*m2[ 5] + m1[10]*m2[ 9] + m1[11]*m2[13];
		m0[10] = m1[ 8]*m2[ 2] + m1[ 9]*m2[ 6] + m1[10]*m2[10] + m1[11]*m2[14];
		m0[11] = m1[ 8]*m2[ 3] + m1[ 9]*m2[ 7] + m1[10]*m2[11] + m1[11]*m2[15];
		
		m0[12] = m1[12]*m2[ 0] + m1[13]*m2[ 4] + m1[14]*m2[ 8] + m1[15]*m2[12];
		m0[13] = m1[12]*m2[ 1] + m1[13]*m2[ 5] + m1[14]*m2[ 9] + m1[15]*m2[13];
		m0[14] = m1[12]*m2[ 2] + m1[13]*m2[ 6] + m1[14]*m2[10] + m1[15]*m2[14];
		m0[15] = m1[12]*m2[ 3] + m1[13]*m2[ 7] + m1[14]*m2[11] + m1[15]*m2[15];
		
	}
	
    public static void lookAt(float [] m, float ex, float ey, float ez, float cx, float cy, float cz, float ux, float uy, float uz) {

        float fx = ex - cx;
        float fy = ey - cy;
        float fz = ez - cz;
        float rlf = 1.f / (float)Math.sqrt(fx*fx + fy*fy + fz*fz);
        m[2] = rlf * fx;
        m[6] = rlf * fy;
        m[10] = rlf * fz;

        float sx = m[10] * uy - m[6] * uz;
        float sy = m[2] * uz - m[10] * ux;
        float sz = m[6] * ux - m[2] * uy;
        float rls = 1.f / (float)Math.sqrt(sx*sx + sy*sy + sz*sz);
        m[0] = rls * sx;
        m[4] = rls * sy;
        m[8] = rls * sz;

        m[1] = m[6] * m[8] - m[10] * m[4];
        m[5] = m[10] * m[0] - m[2] * m[8];
        m[9] = m[2] * m[4] - m[6] * m[0];
        
        m[12] = -(ex * m[0] + ey * m[4] + ez * m[8]);
        m[13] = -(ex * m[1] + ey * m[5] + ez * m[9]);
        m[14] = -(ex * m[2] + ey * m[6] + ez * m[10]);

        m[3] = m[7] = m[11] = 0;
        m[15] = 1;

    }

	public static void perspective(float [] m, float fov, float aspect, float near, float far) {

		float top = near * (float)Math.tan(fov * Math.PI / 360.0);
		float bottom = -top;
		float left = bottom * aspect;
		float right = top * aspect;
		
		m[0] = 2 * near / (right - left);
		m[1] = 0;
		m[2] = 0;
		m[3] = 0;
		m[4] = 0;
		m[5] = 2 * near / (top - bottom);
		m[6] = 0;
		m[7] = 0;
		m[8] = (right + left) / (right - left);
		m[9] = (top + bottom) / (top - bottom);
		m[10] = -(far + near) / (far - near);
		m[11] = -1;
		m[12] = 0;
		m[13] = 0;
		m[14] = -2 * far * near / (far - near);
		m[15] = 0;
		
	}
	
	private WebGLRenderingContext gl;
	private WebGLProgram program;
	private WebGLTexture [] textures = new WebGLTexture[3];
	
}