/**
 * package for collision detection classes
 */
package simogl.collision;

import java.util.ArrayList;

import simogl.gameobjects.AbstractGameObject;
import simogl.geometry.AbstractMesh;
import simogl.geometry.Face;
import simogl.geometry.Point3D;
import simogl.geometry.Triangle;
import simogl.level.Terrain;

/**
 * This class can be used to find different kinds of collisions 
 * @author Frank Bruns
 *
 */
public class CollisionFinder
{
	/**
	 * Creates a new CollisionFinder object
	 *
	 */
	public CollisionFinder()
	{
		
	}
	
	/**
	 * This method checks, if a collision between the two specified meshes
	 * has occured.
	 * @param mesh1 first mesh
	 * @param mesh2 second mesh
	 * @return collision detected? --> true=yes, false=no
	 */
	public boolean collided(AbstractMesh mesh1, AbstractMesh mesh2)
	{
		float posX1 = mesh1.getPositionX();
		float posX2 = mesh2.getPositionX();
		float posWidth1 = posX1 + mesh1.getBoundingBox().getWidth();
		float posWidth2 = posX2 + mesh2.getBoundingBox().getWidth();
		
		float posY1 = mesh1.getPositionY();
		float posY2 = mesh2.getPositionY();
		float posHeight1 = posY1 + mesh1.getBoundingBox().getHeight();
		float posHeight2 = posY2 + mesh2.getBoundingBox().getHeight();
		
		float posZ1 = mesh1.getPositionZ();
		float posZ2 = mesh2.getPositionZ();
		// Need to use minus operator because the bounding boxes length
		// edge goes along the _negative_ z-axis
		float posLength1 = posZ1 - mesh1.getBoundingBox().getLength();
		float posLength2 = posZ2 - mesh2.getBoundingBox().getLength();
		
		if((posWidth1>=posX2 && posX1<=posX2 || 
				posX1<=posWidth2 && posWidth1>=posWidth2
				|| posWidth2>=posX1 && posX2<=posX1 || 
				posX2<=posWidth1 && posWidth2>=posWidth1) &&
				
				(posHeight1>=posY2 && posY1<=posY2 || 
				posY1<=posHeight2 && posHeight1>=posHeight2
				|| posHeight2>=posY1 && posY2<=posY1 || 
				posY2<=posHeight1 && posHeight2>=posHeight1) &&

				(posLength1<=posZ2 && posZ1>=posZ2 || 
				posZ1>=posLength2 && posLength1<=posLength2
				|| posLength2<=posZ1 && posZ2>=posZ1 || 
				posZ2>=posLength1 && posLength2<=posLength1))
		{
			return true;
		}
		else return false;
	}
	
	/**
	 * This method checks, if a collision between the two specified
	 * game objects has occured. This method is pretty similar to the 
	 * one that checks two meshes for collision. The only difference 
	 * between them is that you can specify game object instead of 
	 * directly specifying mesh objects.
	 * @param go1 first game object
	 * @param go2 second game objet
	 * @return collision detected? --> true=yes, false=no
	 */
	public boolean collided(AbstractGameObject go1, AbstractGameObject go2)
	{
		AbstractMesh mesh1 = go1.getModel().getCurrentAnim().getCurrentFrame().getMesh();
		AbstractMesh mesh2 = go2.getModel().getCurrentAnim().getCurrentFrame().getMesh();
		
		float posX1 = mesh1.getPositionX();
		float posX2 = mesh2.getPositionX();
		float posWidth1 = posX1 + mesh1.getBoundingBox().getWidth();
		float posWidth2 = posX2 + mesh2.getBoundingBox().getWidth();
		
		float posY1 = mesh1.getPositionY();
		float posY2 = mesh2.getPositionY();
		float posHeight1 = posY1 + mesh1.getBoundingBox().getHeight();
		float posHeight2 = posY2 + mesh2.getBoundingBox().getHeight();
		
		float posZ1 = mesh1.getPositionZ();
		float posZ2 = mesh2.getPositionZ();
		// Need to use minus operator because the bounding boxes length
		// edge goes along the _negative_ z-axis
		float posLength1 = posZ1 - mesh1.getBoundingBox().getLength();
		float posLength2 = posZ2 - mesh2.getBoundingBox().getLength();
		
		if((posWidth1>=posX2 && posX1<=posX2 || 
				posX1<=posWidth2 && posWidth1>=posWidth2
				|| posWidth2>=posX1 && posX2<=posX1 || 
				posX2<=posWidth1 && posWidth2>=posWidth1) &&
				
				(posHeight1>=posY2 && posY1<=posY2 || 
				posY1<=posHeight2 && posHeight1>=posHeight2
				|| posHeight2>=posY1 && posY2<=posY1 || 
				posY2<=posHeight1 && posHeight2>=posHeight1) &&

				(posLength1<=posZ2 && posZ1>=posZ2 || 
				posZ1>=posLength2 && posLength1<=posLength2
				|| posLength2<=posZ1 && posZ2>=posZ1 || 
				posZ2>=posLength1 && posLength2<=posLength1))
		{
			return true;
		}
		else return false;
	}
	
	/**
	 * This method checks for collision between the specified point and the mesh
	 * @param point point to check for collision with mesh
	 * @param mesh mesh to check for collision with point
	 * @return collision detected? --> true=yes, false=no
	 */
	public boolean collided(Point3D point, AbstractMesh mesh)
	{
		float meshX = mesh.getPositionX();
		float meshY = mesh.getPositionY();
		float meshZ = mesh.getPositionZ();
		float meshWidth = mesh.getBoundingBox().getWidth();
		float meshHeight = mesh.getBoundingBox().getHeight();
		float meshLength = mesh.getBoundingBox().getLength();
						
		if((point.getX() >= meshX && point.getX() <= meshWidth)
				|| (point.getY() >= meshY && point.getY() <= meshHeight)
				|| (point.getZ() <= meshZ && point.getZ() >= meshLength))
		{
			return true;
		}
		else return false;
	}
	
	/**
	 * This method checks for collision between the specified point and the game object
	 * @param point point to check for collision with game object
	 * @param go game object to check for collision with point
	 * @return collision detected? --> true=yes, false=no
	 */
	public boolean collided(Point3D point, AbstractGameObject go)
	{
		float goX = go.getModel().getCurrentAnim().getCurrentFrame().getMesh().getPositionX();
		float goY = go.getModel().getCurrentAnim().getCurrentFrame().getMesh().getPositionY();
		float goZ = go.getModel().getCurrentAnim().getCurrentFrame().getMesh().getPositionZ();
		float goWidth = go.getModel().getCurrentAnim().getCurrentFrame().getMesh().getBoundingBox().getWidth();
		float goHeight = go.getModel().getCurrentAnim().getCurrentFrame().getMesh().getBoundingBox().getHeight();
		float goLength = go.getModel().getCurrentAnim().getCurrentFrame().getMesh().getBoundingBox().getLength();
						
		if((point.getX() >= goX && point.getX() <= goWidth)
				|| (point.getY() >= goY && point.getY() <= goHeight)
				|| (point.getZ() <= goZ && point.getZ() >= goLength))
		{
			return true;
		}
		else return false;
	}
	
	/**
	 * This method checks, if a collision between the bottom center point
	 * of the mesh and the terrain has occured and returns 
	 * the triangle(s) that has/have collided with the mesh.
	 * @param mesh mesh, that could have been collided with the terrain
	 * @param terrain terrain to check collision with
	 * @return ArrayList with the triangles, that have collided with
	 * the bottom center point of the specified mesh
	 */
	public ArrayList<Triangle> collided(AbstractMesh mesh, Terrain terrain)
	{	
		ArrayList<Triangle> collTrigs = new ArrayList<Triangle>();
		
		
		float meshCenterX = mesh.getPositionX() + mesh.getBoundingBox().getWidth()/2f;
		float meshCenterY = 0f;
		float meshCenterZ = mesh.getPositionZ() - mesh.getBoundingBox().getLength()/2f;
		Point3D meshCenter = new Point3D(meshCenterX,meshCenterY,meshCenterZ);
		
		Triangle trig1 = null;
		Triangle trig2 = null;
		
		float vert1YSaver = 0f;
		float vert2YSaver = 0f;
		float vert3YSaver = 0f;
		
		for(Face face : terrain.getFaces())
		{
			trig1 = face.getTriangle1();
			
			//save the original y-value, to be able to restore it later
			vert1YSaver = trig1.getVertex1().getY();
			vert2YSaver = trig1.getVertex2().getY();
			vert3YSaver = trig1.getVertex3().getY();
			
			trig1.getVertex1().setY(0);
			trig1.getVertex2().setY(0);
			trig1.getVertex3().setY(0);
						
			if(this.isPointInTriangle(meshCenter, trig1))
			{
				// restore old y-values
				trig1.getVertex1().setY(vert1YSaver);
				trig1.getVertex2().setY(vert2YSaver);
				trig1.getVertex3().setY(vert3YSaver);
				collTrigs.add(face.getTriangle1());
			}
			else
			{
				// restore old y-values
				trig1.getVertex1().setY(vert1YSaver);
				trig1.getVertex2().setY(vert2YSaver);
				trig1.getVertex3().setY(vert3YSaver);
			}
			
			
			trig2 = face.getTriangle2();
			
			// save the original y-value, to be able to restore it later
			vert1YSaver = trig2.getVertex1().getY();
			vert2YSaver = trig2.getVertex2().getY();
			vert3YSaver = trig2.getVertex3().getY();
			
			trig2.getVertex1().setY(0);
			trig2.getVertex2().setY(0);
			trig2.getVertex3().setY(0);
			
			if(this.isPointInTriangle(meshCenter, trig2))
			{
				// restore old y-values
				trig2.getVertex1().setY(vert1YSaver);
				trig2.getVertex2().setY(vert2YSaver);
				trig2.getVertex3().setY(vert3YSaver);
				collTrigs.add(face.getTriangle2());
			}
			else
			{
				// restore old y-values
				trig2.getVertex1().setY(vert1YSaver);
				trig2.getVertex2().setY(vert2YSaver);
				trig2.getVertex3().setY(vert3YSaver);
			}
		}
		
		return collTrigs;
	}
	
	/**
	 * This method checks, if a collision between a point and the 
	 * terrain has occured. To be precisely, it finds the trangle(s)
	 * this point is located on and returns the involved triangle(s).
	 * @param point point to check collision with
	 * @param terrain terrain to check collision with
	 * @return triangle(s) the specified point is located on 
	 */
	public ArrayList<Triangle> collided(Point3D point, Terrain terrain)
	{
		Point3D currPoint = point;
		float currPointSaver = currPoint.getY();		
		currPoint.setY(0f);
		
		// saves the triangles in question for checking
		ArrayList<Triangle> possibleTrigs = new ArrayList<Triangle>();
		
		// find some possible triangles for later collision checking
		// it is some kind of filter function to reduce the number of
		// triangles that need to be checked for collision. The result
		// is a remarkable increase of speed in comparision to the old
		// more naiv method I used before.
		float terrRes = terrain.getResolution();
		for(Face face : terrain.getFaces())
		{
			float t1v1x = face.getTriangle1().getVertex1().getX();
			float t1v2x = face.getTriangle1().getVertex2().getX();
			float t1v3x = face.getTriangle1().getVertex3().getX();
			float t2v1x = face.getTriangle2().getVertex1().getX();
			float t2v2x = face.getTriangle2().getVertex2().getX();
			float t2v3x = face.getTriangle2().getVertex3().getX();
			float t1v1z = face.getTriangle1().getVertex1().getZ();
			float t1v2z = face.getTriangle1().getVertex2().getZ();
			float t1v3z = face.getTriangle1().getVertex3().getZ();
			float t2v1z = face.getTriangle2().getVertex1().getZ();
			float t2v2z = face.getTriangle2().getVertex2().getZ();
			float t2v3z = face.getTriangle2().getVertex3().getZ();
			
			// find potential triangle in face's first triangle
			if((t1v1x > currPoint.getX()-terrRes 
					&& t1v1x < currPoint.getX()+terrRes)
					&&
					(t1v2x > currPoint.getX()-terrRes 
						&& t1v2x < currPoint.getX()+terrRes)
					&&
					(t1v3x > currPoint.getX()-terrRes 
						&& t1v3x < currPoint.getX()+terrRes)
					||
					(t1v1z > currPoint.getZ()+terrRes 
						&& t1v1z < currPoint.getZ()-terrRes)
					&&
					(t1v2z > currPoint.getZ()+terrRes 
						&& t1v2z < currPoint.getZ()-terrRes)
					&&
					(t1v3z > currPoint.getZ()+terrRes 
						&& t1v3z < currPoint.getZ()-terrRes))
			{
				possibleTrigs.add(face.getTriangle1());
			}
			
			// find potential triangle in face's second triangle
			if((t2v1x > currPoint.getX()-terrRes 
					&& t2v1x < currPoint.getX()+terrRes)
					&&
					(t2v2x > currPoint.getX()-terrRes 
						&& t2v2x < currPoint.getX()+terrRes)
					&&
					(t2v3x > currPoint.getX()-terrRes 
						&& t2v3x < currPoint.getX()+terrRes)
					||
					(t2v1z > currPoint.getZ()+terrRes 
						&& t2v1z < currPoint.getZ()-terrRes)
					&&
					(t2v2z > currPoint.getZ()+terrRes 
						&& t2v2z < currPoint.getZ()-terrRes)
					&&
					(t2v3z > currPoint.getZ()+terrRes 
						&& t2v3z < currPoint.getZ()-terrRes))
			{
				possibleTrigs.add(face.getTriangle2());
			}			
		}		
		
		// saves the triangles the point is located in
		ArrayList<Triangle> collTrigs = new ArrayList<Triangle>();

		Triangle trig1 = null;
		
		float vert1YSaver = 0f;
		float vert2YSaver = 0f;
		float vert3YSaver = 0f;
		
		for(Triangle possTrig : possibleTrigs)
		{
			trig1 = possTrig;
			
			//save the original y-value, to be able to restore it later
			vert1YSaver = trig1.getVertex1().getY();			
			vert2YSaver = trig1.getVertex2().getY();
			vert3YSaver = trig1.getVertex3().getY();
			
			trig1.getVertex1().setY(0);
			trig1.getVertex2().setY(0);
			trig1.getVertex3().setY(0);
						
			if(this.isPointInTriangle(currPoint, trig1))
			{				
				// restore old y-values
				trig1.getVertex1().setY(vert1YSaver);
				trig1.getVertex2().setY(vert2YSaver);
				trig1.getVertex3().setY(vert3YSaver);
				collTrigs.add(possTrig);
			}
			else
			{
				// restore old y-values
				trig1.getVertex1().setY(vert1YSaver);
				trig1.getVertex2().setY(vert2YSaver);
				trig1.getVertex3().setY(vert3YSaver);
			}
		}
		
		currPoint.setY(currPointSaver);
					
		return collTrigs;
	}
	
	/**
	 * This helper method checks, if the specified point lies within
	 * the specified triangle
	 * @param p point to check
	 * @param trig triangle to check
	 * @return
	 */
	private boolean isPointInTriangle(Point3D p, Triangle trig)
	{
		Point3D v1 = trig.getVertex1();
		Point3D v2 = trig.getVertex2();
		Point3D v3 = trig.getVertex3();
				
		if(isPointOnSameSide(p,v1,v2,v3) && isPointOnSameSide(p,v2,v1,v3)
				&& isPointOnSameSide(p,v3,v1,v2))
		{
			return true;
		}
		else return false;
	}
	
	/**
	 * This helper method is used to determine, if the first point is
	 * located on the same side of a triangle's line than the other
	 * points
	 * @param p1 point 1
	 * @param p2 point 2
	 * @param v1 vertex 1
	 * @param v2 vertex 2
	 * @return is the point on the same side? --> true=yes, false=no
	 */
	private boolean isPointOnSameSide(Point3D p1, Point3D p2, Point3D v1, Point3D v2)
	{
		Point3D cp1 = v1.getCrossProduct(v2.subtract(v2,v1),p1.subtract(p1,v1));
	    Point3D cp2 = v2.getCrossProduct(v2.subtract(v2,v1),p2.subtract(p2,v1));
	    
	    if(cp1.getDotProduct(cp1,cp2) >= 0) return true;
	    else return false;	    	
	}
}
