package simogl.collision;

import java.nio.IntBuffer;
import java.util.ArrayList;

import javax.media.opengl.GL;
import javax.media.opengl.glu.GLU;

import com.sun.opengl.util.BufferUtil;

import simogl.camera.Camera;
import simogl.gameobjects.AbstractGameObject;

/**
 * This class can be used to create objects that encapsulate picking
 * operations on object of the scene
 * @author Frank Bruns
 *
 */
public class ObjectPicker
{
	private ArrayList<AbstractGameObject> gameObjects = null;
	private ArrayList<PickingHit> hits = null;
	private Camera cam = null;
	private float pickDiameter = 0f;	
	
	/**
	 * Creates a new picking object for the specified list of game objects
	 * @param gameObjects list of game objects that can be picked with
	 * this picking object
	 * @param cam camera object
	 * @param pickDiameter diameter of the picking area around the mouse
	 */
	public ObjectPicker(ArrayList<AbstractGameObject> gameObjects, Camera cam, float pickDiameter)
	{
		this.gameObjects = gameObjects;
		this.cam = cam;
		this.pickDiameter = pickDiameter;
		this.hits = new ArrayList<PickingHit>();
	}
	
	/**
	 * Performs the operations neccessary for OpenGL picking.
	 * @param gl
	 * @param mouseX mouse x-position
	 * @param mouseY mouse y-position
	 * @return picked some object? --> true=yes, false=no
	 */
	private boolean perfomPicking(GL gl, int mouseX, int mouseY)	
	{
		GLU glu = new GLU();
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glPushMatrix();
		gl.glLoadIdentity();
		
		// create the pick matrix
		int[] viewport = new int[4];
		gl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);
		glu.gluPickMatrix(mouseX, viewport[3] - (double) mouseY, this.pickDiameter,
				this.pickDiameter, viewport, 0);
		
		// set up regular projection matrix
		glu.gluPerspective(this.cam.getViewingAngle(), this.cam.getAspectRatio(), this.cam.getNearPlane(), this.cam.getFarPlane());
		gl.glMatrixMode(GL.GL_MODELVIEW);

		IntBuffer selectBuff = BufferUtil.newIntBuffer(1024);
		gl.glSelectBuffer(selectBuff.capacity(), selectBuff);

		// change to selection mode
		gl.glRenderMode(GL.GL_SELECT);
		gl.glInitNames();

		// draw the buildings in the scene and assign names
		for (int i = 0; i < this.gameObjects.size(); i++)
		{
			gl.glPushName(i);
			this.gameObjects.get(i).getModel().getCurrentAnim()
					.getCurrentFrame().getMesh().draw(gl);
			gl.glPopName();
		}
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glPopMatrix();
		gl.glMatrixMode(GL.GL_MODELVIEW);

		// return to render mode
		int numHits = gl.glRenderMode(GL.GL_RENDER);

		//only evaluate hits, if the number of hits is > 0 
		if (numHits > 0)
		{
			int index = 0;		
			for (int i = 0; i < numHits; i++)
			{
				PickingHit pickHit = new PickingHit();				
				pickHit.setNameCount(selectBuff.get(index++));
				pickHit.setMinDepth(selectBuff.get(index++));
				pickHit.setMaxDepth(selectBuff.get(index++));
				pickHit.setID(selectBuff.get(index++));	
				this.hits.add(pickHit);
			}
			return true;
		}
		else return false;
	}
	
	/**
	 * Returns the game object that lies in front to the viewer at most.
	 * If no object was picked, the method returns NULL
	 * @param gl
	 * @param mouseX mouse x-position
	 * @param mouseY mouse y-position 
	 * @return game object in front
	 */
	public AbstractGameObject getObjectInFront(GL gl, int mouseX, int mouseY)
	{
		if(this.gameObjects.size() > 0)
		{
			if(this.perfomPicking(gl, mouseX, mouseY))
			{
				// look for the least minDepth value, because the object with
				// the least minDepth value will be the one in front at most
				int minDepth = this.hits.get(0).getMinDepth();
				for(PickingHit hit : this.hits)
				{
					minDepth = Math.min(minDepth,hit.getMinDepth());
				}
				
				// find the object according to the found minDepth value
				int objIndex = -1;
				for(PickingHit hit : this.hits)
				{
					if(hit.getMinDepth() == minDepth)
					{						
						objIndex = hit.getID();
						break;
					}			
				}
				// clear list with hits for next use
				this.hits.clear();								
				return this.gameObjects.get(objIndex);	
			}		
			else return null;
		}
		else return null;
		
	}
	
	/**
	 * Returns the game object that lies at back to the viewer at most
	 * @param gl
	 * @param mouseX mouse x-position
	 * @param mouseY mouse y-position 
	 * @return game object at back
	 */
	public AbstractGameObject getObjectAtBack(GL gl, int mouseX, int mouseY)
	{
		if(this.gameObjects.size() > 0)
		{
			if(this.perfomPicking(gl, mouseX, mouseY))
			{
				// look for the greatest minDepth value, because the object with
				// the greatest minDepth value will be the at back at most
				int minDepth = this.hits.get(0).getMinDepth();
				for(PickingHit hit : this.hits)
				{
					minDepth = Math.max(minDepth,hit.getMinDepth());
				}
				
				// find the object according to the found minDepth value
				int objIndex = -1;
				for(PickingHit hit : this.hits)
				{
					if(hit.getMinDepth() == minDepth)
					{						
						objIndex = hit.getID();
						break;
					}			
				}
				// clear list with hits for next use
				this.hits.clear();								
				return this.gameObjects.get(objIndex);	
			}		
			else return null;
		}
		else return null;
	}
}
