package net.virtuallyabstract.demo1

import javax.media.opengl._
import javax.media.opengl.awt._
import javax.media.opengl.fixedfunc._
import com.jogamp.newt.event.awt._
import com.jogamp.opengl.util._
import com.jogamp.opengl.util.glsl._;
import java.util._
import java.awt._
import java.awt.event._
import javax.swing._
import scala.io._
import java.nio._;


object ShaderLoader
{
	def createShader(gl: GL4, shaderType: Int, context: Class[_])(filename: String): ShaderCode = {
		ShaderCode.create(gl, shaderType, 1, context, Array(filename), false);
	}
	
	def createProgram(gl: GL4, shaders: ShaderCode*): ShaderProgram = {
		val retVal: ShaderProgram = new ShaderProgram();
		retVal.init(gl);
		
		for(shader <- shaders)
		{
			retVal.add(shader);
		}
		
		return retVal;
	}
}

object BufferUtils
{
	def createBuffer(gl: GL4): Int = {
		val bufferArray: IntBuffer = IntBuffer.allocate(1);					
		
		gl.glGenBuffers(1, bufferArray);
		
		return bufferArray.get();
	}
	
	def createVAO(gl: GL4): Int = {
  		val vaoArray: IntBuffer = IntBuffer.allocate(1);							
  		
		gl.glGenVertexArrays(1, vaoArray);		
		
		return vaoArray.get();
	}
	
	def setBufferData(gl: GL4, bufferID: Int, data: FloatBuffer): Unit = {
		//glBindBuffer sets the active array buffer to be manipulated.
		gl.glBindBuffer(GL.GL_ARRAY_BUFFER, bufferID);
		
		//Using GLBuffers utility class to provide the size of the float primitive so I can calculate the size
		val size: Long = data.limit() * GLBuffers.sizeOfGLType(GL.GL_FLOAT);
		gl.glBufferData(GL.GL_ARRAY_BUFFER, size, data, GL.GL_DYNAMIC_DRAW);
	}
	
	def addVertex(vertices: FloatBuffer)(x: Float, y: Float, z: Float): Unit = {
		vertices.put(x);
		vertices.put(y);
		vertices.put(z);	
		vertices.put(1.0f);
	}
}

//Normalized Device Space
//The region defined by the following coordinate limits:
//   -1.0 to 1.0 on the X, Y and Z axis (where the positive Z axis is coming out of the screen towards you).
//Anything that falls within that region will be displayed
//
//Once vertices are mapped to those coordinates they will be translated into 'Window coordinates' based on
//the values of glViewPort and glDepthRange
class Cube(val gl: GL4)
{  
	var bufferID: Int = -1;
	var normalID: Int = -1;
	var colorID: Int = -1;
	var vaoID: Int = -1;
	var vertexCount: Int = 0;
	
	init();
  
	def init(): Unit = {
		bufferID = BufferUtils.createBuffer(gl);
		normalID = BufferUtils.createBuffer(gl);
		colorID = BufferUtils.createBuffer(gl);
		
		//2 triangles per face, 3 vertices per triangle, 4 elements per vertex, 6 faces = 144 vertices
		vertexCount = 144;
		
		val vertices: FloatBuffer = FloatBuffer.allocate(vertexCount);
		val normals: FloatBuffer = FloatBuffer.allocate(vertexCount);
		val colors: FloatBuffer = FloatBuffer.allocate(vertexCount);
		val addVertex = BufferUtils.addVertex(vertices)_;
		val addNormal = BufferUtils.addVertex(normals)_;
		val addColor = BufferUtils.addVertex(colors)_;
			
		vertices.clear();
		normals.clear();
		colors.clear();
		
		addVertex(-0.5f, -0.5f, 0.5f);
		addVertex(0.5f, 0.5f, 0.5f);
		addVertex(-0.5f, 0.5f, 0.5f);		
		addVertex(-0.5f, -0.5f, 0.5f);
		addVertex(0.5f, -0.5f, 0.5f);
		addVertex(0.5f, 0.5f, 0.5f);
		for(n <- 0 to 5)
		{
			addNormal(0.0f, 0.0f, 1.0f);
			addColor(1.0f, 1.0f, 0.0f);
		}
		
		addVertex(-0.5f, -0.5f, -0.5f);
		addVertex(-0.5f, 0.5f, -0.5f);		
		addVertex(0.5f, 0.5f, -0.5f);
		addVertex(-0.5f, -0.5f, -0.5f);
		addVertex(0.5f, 0.5f, -0.5f);
		addVertex(0.5f, -0.5f, -0.5f);
		for(n <- 0 to 5)
		{
			addNormal(0.0f, 0.0f, -1.0f);
			addColor(0.0f, 1.0f, 0.0f);
		}
		
		addVertex(-0.5f, -0.5f, -0.5f);
		addVertex(-0.5f, 0.5f, 0.5f);
		addVertex(-0.5f, 0.5f, -0.5f);
		addVertex(-0.5f, -0.5f, -0.5f);
		addVertex(-0.5f, -0.5f, 0.5f);
		addVertex(-0.5f, 0.5f, 0.5f);
		for(n <- 0 to 5)
		{
			addNormal(-1.0f, 0.0f, 0.0f);
			addColor(0.0f, 0.0f, 1.0f);
		}
		
		addVertex(0.5f, -0.5f, -0.5f);
		addVertex(0.5f, 0.5f, -0.5f);
		addVertex(0.5f, 0.5f, 0.5f);
		addVertex(0.5f, -0.5f, -0.5f);
		addVertex(0.5f, 0.5f, 0.5f);
		addVertex(0.5f, -0.5f, 0.5f);
		for(n <- 0 to 5)
		{
			addNormal(1.0f, 0.0f, 0.0f);
			addColor(0.0f, 0.5f, 0.5f);
		}
		
		addVertex(-0.5f, -0.5f, -0.5f);
		addVertex(0.5f, -0.5f, -0.5f);
		addVertex(0.5f, -0.5f, 0.5f);
		addVertex(-0.5f, -0.5f, -0.5f);
		addVertex(0.5f, -0.5f, 0.5f);
		addVertex(-0.5f, -0.5f, 0.5f);
		for(n <- 0 to 5)
		{
			addNormal(0.0f, -1.0f, 0.0f);
			addColor(0.5f, 0.5f, 0.0f);
		}
		
		addVertex(-0.5f, 0.5f, -0.5f);
		addVertex(0.5f, 0.5f, 0.5f);
		addVertex(0.5f, 0.5f, -0.5f);
		addVertex(-0.5f, 0.5f, -0.5f);
		addVertex(-0.5f, 0.5f, 0.5f);
		addVertex(0.5f, 0.5f, 0.5f);
		for(n <- 0 to 5)
		{
			addNormal(0.0f, 1.0f, 0.0f);
			addColor(0.5f, 0.0f, 0.5f);
		}
					
		vertices.flip();
		normals.flip();
		colors.flip();
		
		BufferUtils.setBufferData(gl, bufferID, vertices);	
		
		//Now that the vertex buffer object (VBO) is constructed we need to create a vertex array object (VAO)
		//The difference between the two is the VAO contains additional meta-data that describes
		//the formatting of the contents of the VBO.
		vaoID = BufferUtils.createVAO(gl);
		
		gl.glBindVertexArray(vaoID);
						
 		//Attribute index 0 will obtain its data from whatever buffer is currently bound to GL_ARRAY_BUFFER and read it according
  		//to the following specifications.
  		gl.glVertexAttribPointer(0, 4, GL.GL_FLOAT, false, 0, 0);
  		gl.glEnableVertexAttribArray(0);
  		
  		//setBufferData has the side-effect of changing the bound VBO
  		BufferUtils.setBufferData(gl, normalID, normals);  		
  		
  		gl.glVertexAttribPointer(1, 4, GL.GL_FLOAT, false, 0, 0);
  		gl.glEnableVertexAttribArray(1);
  		
  		BufferUtils.setBufferData(gl, colorID, colors);
  		
  		gl.glVertexAttribPointer(2, 4, GL.GL_FLOAT, false, 0, 0);
  		gl.glEnableVertexAttribArray(2);
	}
	
	def draw(): Unit = {	  
		gl.glBindVertexArray(vaoID);
				
		//Draws the currently bound vertex array object
		gl.glDrawArrays(GL.GL_TRIANGLES, 0, vertexCount);		
	}
}

class Demo1 extends GLEventListener
{
	var program: ShaderProgram = null;
	var cube: Cube = null;
	var mvp: PMVMatrix = null;
	
	override def init(drawable: GLAutoDrawable): Unit = {
		val gl: GL4 = drawable.getGL().getGL4();
		
		//Enable culling, by default only triangles with vertices ordered in a counter clockwise order will be rendered
		gl.glEnable(GL.GL_CULL_FACE);		
		
		val createVertexShader = ShaderLoader.createShader(gl, GL2ES2.GL_VERTEX_SHADER, this.getClass())_;
		val createFragmentShader = ShaderLoader.createShader(gl, GL2ES2.GL_FRAGMENT_SHADER, this.getClass())_;
		
		try
		{
			val vShader: ShaderCode = createVertexShader("shaders/vshader.glsl");
			if(vShader.compile(gl, System.out) == false)
				throw new Exception("Failed to compile vertex shader");
			
			val fShader: ShaderCode = createFragmentShader("shaders/fshader.glsl");
			if(fShader.compile(gl, System.out) == false)
				throw new Exception("Failed to compile fragment shader");
			
			program = ShaderLoader.createProgram(gl, vShader, fShader);
			if(program.link(gl, System.out) == false)
				throw new Exception("Failed to link program");
			
			//No longer needed
			vShader.destroy(gl);
			fShader.destroy(gl);
			
			cube = new Cube(gl);
			
			mvp = new PMVMatrix();
		}
		catch
		{
			case e: Exception => {
				e.printStackTrace();
				System.exit(1);
			}
		}
	}

	override def display(drawable: GLAutoDrawable): Unit = {
		val gl: GL4 = drawable.getGL().getGL4();

		val redArray: Array[Float] = Array(1.0f, 0.0f, 0.0f, 1.0f);
		gl.glClearBufferfv(GL2ES3.GL_COLOR, 0, redArray, 0);
		
		program.useProgram(gl, true);
					
		mvp.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
		mvp.glLoadIdentity();

		val time: Long = System.currentTimeMillis();
		val degrees: Float = ((time / 10) % 360);

		mvp.glTranslatef(0.0f, 0.0f, -5.0f);
		mvp.glRotatef(degrees, 1.0f, 1.0f, 0.0f);
		mvp.update();				
		
		//Modelview matrix
		gl.glUniformMatrix4fv(3, 1, false, mvp.glGetMatrixf());

		//Normal matrix (transpose of the inverse modelview matrix)
		gl.glUniformMatrix4fv(4, 1, false, mvp.glGetMvitMatrixf());
		
		mvp.glMatrixMode(GLMatrixFunc.GL_PROJECTION);
		mvp.glLoadIdentity();
		//90 degree field of view, aspect ratio of 1.0 (since window width = height), zNear 1.0f, zFar 20.0f
		mvp.gluPerspective(90.0f, 1.0f, 1.0f, 20.0f);
		mvp.update();
		
		//Projection matrix
		gl.glUniformMatrix4fv(5, 1, false, mvp.glGetMatrixf());
			
		cube.draw();
	}

	override def reshape(drawable: GLAutoDrawable, x: Int, y: Int, width: Int, height: Int): Unit = {
	}

	override def dispose(drawable: GLAutoDrawable): Unit = {
  		val gl: GL4 = drawable.getGL().getGL4();
  		
		if(program != null)
		{
			program.destroy(gl);
		}
	}
}

object EntryPoint {
	def main(args: Array[String]): Unit = {
		val frameMain: Frame = new Frame("Main Window");
		frameMain.setLayout(new BorderLayout());

		val animator: Animator = new Animator();
		frameMain.addWindowListener(new WindowAdapter() {
			override def windowClosing(e: WindowEvent) = {
				System.exit(0);
			}
		});

		val canvas: GLCanvas = new GLCanvas();
		animator.add(canvas);

		canvas.addGLEventListener(new Demo1());

		frameMain.setSize(600, 600);
		frameMain.setLocation(200, 200);
		frameMain.add(canvas, BorderLayout.CENTER);
		frameMain.validate();
		frameMain.setVisible(true);
		
		animator.start();
	}
}